@gathertown/uikit-react-native
Version:
Sendbird UIKit for React Native: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.
145 lines (125 loc) • 5.21 kB
text/typescript
import { useEffect, useRef, useState } from 'react';
import type { NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData } from 'react-native';
import { Platform } from 'react-native';
import { SendbirdFileMessage, SendbirdUserMessage, replace, useFreshCallback } from '@gathertown/uikit-utils';
import type { MentionedUser } from '../types';
import { useSendbirdChat } from './useContext';
const useMentionTextInput = (params: { messageToEdit?: SendbirdUserMessage | SendbirdFileMessage }) => {
const { mentionManager, sbOptions } = useSendbirdChat();
const mentionedUsersRef = useRef<MentionedUser[]>([]);
const textInputRef = useRef<TextInput>();
const [text, setText] = useState('');
const [selection, setSelection] = useState({ start: 0, end: 0 });
// TODO: Refactor text edit logic more clearly
useEffect(() => {
if (
mentionManager.shouldUseMentionedMessageTemplate(
params.messageToEdit,
sbOptions.uikit.groupChannel.channel.enableMention,
)
) {
const result = mentionManager.templateToTextAndMentionedUsers(
params.messageToEdit?.mentionedMessageTemplate ?? '',
params.messageToEdit?.mentionedUsers ?? [],
);
mentionedUsersRef.current = result.mentionedUsers;
setText(result.mentionedText);
} else {
mentionedUsersRef.current = [];
if (params.messageToEdit?.isUserMessage()) {
setText(params.messageToEdit.message);
}
}
}, [params.messageToEdit]);
const onChangeText = useFreshCallback((_nextText: string, addedMentionedUser?: MentionedUser) => {
const prevText = text;
let nextText = _nextText;
let offset = nextText.length - prevText.length;
// Text clear
if (nextText === '') {
mentionedUsersRef.current = [];
}
// Text add
else if (offset > 0) {
/** Add mentioned user **/
if (addedMentionedUser) mentionedUsersRef.current.push(addedMentionedUser);
/** Reconcile mentioned users range on the right side of the selection **/
mentionedUsersRef.current = mentionManager.reconcileRangeOfMentionedUsers(
offset,
selection.end,
mentionedUsersRef.current,
);
}
// Text remove
else if (offset < 0) {
// Ranged remove
if (selection.start !== selection.end) {
/** Filter mentioned users in selection range **/
const { filtered, lastSelection } = mentionManager.removeMentionedUsersInSelection(
selection,
mentionedUsersRef.current,
);
/** Reconcile mentioned users range on the right side of the selection **/
mentionedUsersRef.current = mentionManager.reconcileRangeOfMentionedUsers(
offset,
Math.max(selection.end, lastSelection),
filtered,
);
}
// Single remove
else {
/** Find mentioned user who ranges in removed selection **/
const foundIndex = mentionedUsersRef.current.findIndex((it) =>
mentionManager.rangeHelpers.overlaps(it.range, selection, 'underMore'),
);
/** If found, remove from the mentioned user list and remove remainder text **/
if (foundIndex > -1) {
const it = mentionedUsersRef.current[foundIndex];
const remainderLength = it.range.end - it.range.start + offset;
offset = -remainderLength + offset;
nextText = replace(nextText, it.range.start, it.range.start + remainderLength, '');
mentionedUsersRef.current.splice(foundIndex, 1);
}
/** Reconcile mentioned users range on the right side of the selection **/
mentionedUsersRef.current = mentionManager.reconcileRangeOfMentionedUsers(
offset,
selection.end,
mentionedUsersRef.current,
);
}
}
setText(nextText);
});
return {
textInputRef,
selection,
onSelectionChange: useFreshCallback((e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
const nativeSelection = { ...e.nativeEvent.selection };
// NOTE: To synchronize call onSelectionChange after onChangeText called on each platform.
setTimeout(() => {
const mentionedUser = mentionedUsersRef.current.find((it) =>
mentionManager.rangeHelpers.overlaps(it.range, nativeSelection),
);
// Selection should be blocked if changed into mentioned area
if (mentionedUser) {
const selectionBlock = { start: mentionedUser.range.start, end: mentionedUser.range.end };
textInputRef.current?.setNativeProps({ selection: selectionBlock });
// BUG: setNativeProps called again when invoked onChangeText
// https://github.com/facebook/react-native/issues/33520
if (Platform.OS === 'android') {
setTimeout(() => {
textInputRef.current?.setNativeProps({ selection: { start: 0 } });
}, 250);
}
setSelection(selectionBlock);
} else {
setSelection(nativeSelection);
}
}, 10);
}),
text,
onChangeText,
mentionedUsers: mentionedUsersRef.current,
};
};
export default useMentionTextInput;