Skip to content

Commit

Permalink
Show current user mention in MarkdownTextInput with green outline
Browse files Browse the repository at this point in the history
  • Loading branch information
Kicu committed Dec 16, 2024
1 parent 653af04 commit b85d761
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 7 deletions.
61 changes: 54 additions & 7 deletions src/components/RNMarkdownTextInput.tsx
Original file line number Diff line number Diff line change
@@ -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';

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

type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & MarkdownTextInput & HTMLInputElement;

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

function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputProps, ref: ForwardedRef<AnimatedMarkdownTextInputRef>) {
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<AnimatedMarkdownTextInputRef>) {
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 (
<AnimatedMarkdownTextInput
allowFontScaling={false}
textBreakStrategy="simple"
keyboardAppearance={theme.colorScheme}
parser={parseExpensiMark}
parser={parserFunction}
ref={(refHandle) => {
if (typeof ref !== 'function') {
return;
}
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
*/
Expand All @@ -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};
63 changes: 63 additions & 0 deletions tests/unit/decorateRangesWithCurrentUserTest.ts
Original file line number Diff line number Diff line change
@@ -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,
},
]);
});
});

0 comments on commit b85d761

Please sign in to comment.