@qte/react-native-gifted-chat
Version:
Performant fork of react-native-gifted-chat with FlashList support
296 lines • 12.8 kB
JavaScript
import React, { createRef, useEffect, useMemo, useRef, useState, useCallback, } from 'react';
import { ActionSheetProvider, } from '@expo/react-native-action-sheet';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { Platform, View, } from 'react-native';
import { Actions } from '../Actions';
import { Avatar } from '../Avatar';
import Bubble from '../Bubble';
import { Composer } from '../Composer';
import { MAX_COMPOSER_HEIGHT, MIN_COMPOSER_HEIGHT, TEST_ID } from '../Constant';
import { Day } from '../Day';
import { GiftedAvatar } from '../GiftedAvatar';
import { GiftedChatContext } from '../GiftedChatContext';
import { InputToolbar } from '../InputToolbar';
import { LoadEarlier } from '../LoadEarlier';
import Message from '../Message';
import MessageContainer from '../MessageContainer';
import { MessageImage } from '../MessageImage';
import { MessageText } from '../MessageText';
import { Send } from '../Send';
import { SystemMessage } from '../SystemMessage';
import { Time } from '../Time';
import * as utils from '../utils';
import Animated, { useAnimatedStyle, useAnimatedReaction, useSharedValue, withTiming, runOnJS, } from 'react-native-reanimated';
import { KeyboardProvider, useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
import stylesCommon from '../styles';
import styles from './styles';
dayjs.extend(localizedFormat);
function GiftedChat(props) {
const { messages = [], initialText = '', isTyping,
// "random" function from here: https://stackoverflow.com/a/8084248/3452513
// we do not use uuid since it would add extra native dependency (https://www.npmjs.com/package/react-native-get-random-values)
// lib's user can decide which algorithm to use and pass it as a prop
messageIdGenerator = () => (Math.random() + 1).toString(36).substring(7), user = {}, onSend, locale = 'en', renderLoading, actionSheet = null, textInputProps, renderChatFooter = null, renderInputToolbar = null, bottomOffset = 0, focusOnInputWhenOpeningKeyboard = true, keyboardShouldPersistTaps = Platform.select({
ios: 'never',
android: 'always',
default: 'never',
}), onInputTextChanged = null, maxInputLength = null, inverted = true, minComposerHeight = MIN_COMPOSER_HEIGHT, maxComposerHeight = MAX_COMPOSER_HEIGHT, isKeyboardInternallyHandled = true, } = props;
const actionSheetRef = useRef(null);
const messageContainerRef = useMemo(() => props.messageContainerRef || createRef(), [props.messageContainerRef]);
const textInputRef = useMemo(() => props.textInputRef || createRef(), [props.textInputRef]);
const isTextInputWasFocused = useRef(false);
const [isInitialized, setIsInitialized] = useState(false);
const [composerHeight, setComposerHeight] = useState(minComposerHeight);
const [text, setText] = useState(() => props.text || '');
const [isTypingDisabled, setIsTypingDisabled] = useState(false);
const keyboard = useReanimatedKeyboardAnimation();
const trackingKeyboardMovement = useSharedValue(false);
const debounceEnableTypingTimeoutId = useRef(undefined);
const keyboardOffsetBottom = useSharedValue(0);
const contentStyleAnim = useAnimatedStyle(() => ({
transform: [
{ translateY: keyboard.height.value - keyboardOffsetBottom.value },
],
}), [keyboard, keyboardOffsetBottom]);
const getTextFromProp = useCallback((fallback) => {
if (props.text === undefined)
return fallback;
return props.text;
}, [props.text]);
/**
* Store text input focus status when keyboard hide to retrieve
* it afterwards if needed.
* `onKeyboardWillHide` may be called twice in sequence so we
* make a guard condition (eg. showing image picker)
*/
const handleTextInputFocusWhenKeyboardHide = useCallback(() => {
if (!isTextInputWasFocused.current)
isTextInputWasFocused.current =
textInputRef.current?.isFocused() || false;
}, [textInputRef]);
/**
* Refocus the text input only if it was focused before showing keyboard.
* This is needed in some cases (eg. showing image picker).
*/
const handleTextInputFocusWhenKeyboardShow = useCallback(() => {
if (textInputRef.current &&
isTextInputWasFocused.current &&
!textInputRef.current.isFocused())
textInputRef.current.focus();
// Reset the indicator since the keyboard is shown
isTextInputWasFocused.current = false;
}, [textInputRef]);
const disableTyping = useCallback(() => {
clearTimeout(debounceEnableTypingTimeoutId.current);
setIsTypingDisabled(true);
}, []);
const enableTyping = useCallback(() => {
clearTimeout(debounceEnableTypingTimeoutId.current);
setIsTypingDisabled(false);
}, []);
const debounceEnableTyping = useCallback(() => {
clearTimeout(debounceEnableTypingTimeoutId.current);
debounceEnableTypingTimeoutId.current = setTimeout(() => {
enableTyping();
}, 50);
}, [enableTyping]);
const scrollToBottom = useCallback((isAnimated = false) => {
if (!messageContainerRef?.current)
return;
messageContainerRef.current.scrollToEnd({ animated: isAnimated });
}, [messageContainerRef]);
const renderMessages = useMemo(() => {
if (!isInitialized)
return null;
const { messagesContainerStyle, ...messagesContainerProps } = props;
const fragment = (<View style={[stylesCommon.fill, messagesContainerStyle]}>
<MessageContainer {...messagesContainerProps} invertibleScrollViewProps={{
inverted,
keyboardShouldPersistTaps,
}} messages={messages} forwardRef={messageContainerRef} isTyping={isTyping}/>
{renderChatFooter?.()}
</View>);
return fragment;
}, [
isInitialized,
isTyping,
messages,
props,
inverted,
keyboardShouldPersistTaps,
messageContainerRef,
renderChatFooter,
]);
const notifyInputTextReset = useCallback(() => {
onInputTextChanged?.('');
}, [onInputTextChanged]);
const resetInputToolbar = useCallback(() => {
textInputRef.current?.clear();
notifyInputTextReset();
setComposerHeight(minComposerHeight);
setText(getTextFromProp(''));
enableTyping();
}, [
minComposerHeight,
getTextFromProp,
textInputRef,
notifyInputTextReset,
enableTyping,
]);
const _onSend = useCallback((messages = [], shouldResetInputToolbar = false) => {
if (!Array.isArray(messages))
messages = [messages];
const newMessages = messages.map(message => {
return {
...message,
user: user,
createdAt: new Date(),
_id: messageIdGenerator?.(),
};
});
if (shouldResetInputToolbar === true) {
disableTyping();
resetInputToolbar();
}
onSend?.(newMessages);
setTimeout(() => scrollToBottom(), 10);
}, [messageIdGenerator, onSend, user, resetInputToolbar, disableTyping, scrollToBottom]);
const onInputSizeChanged = useCallback((size) => {
const newComposerHeight = Math.max(minComposerHeight, Math.min(maxComposerHeight, size.height));
setComposerHeight(newComposerHeight);
}, [maxComposerHeight, minComposerHeight]);
const _onInputTextChanged = useCallback((_text) => {
if (isTypingDisabled)
return;
onInputTextChanged?.(_text);
// Only set state if it's not being overridden by a prop.
if (props.text === undefined)
setText(_text);
}, [onInputTextChanged, isTypingDisabled, props.text]);
const onInitialLayoutViewLayout = useCallback((e) => {
if (isInitialized)
return;
const { layout } = e.nativeEvent;
if (layout.height <= 0)
return;
notifyInputTextReset();
setIsInitialized(true);
setComposerHeight(minComposerHeight);
setText(getTextFromProp(initialText));
}, [isInitialized, initialText, minComposerHeight, notifyInputTextReset, getTextFromProp]);
const inputToolbarFragment = useMemo(() => {
if (!isInitialized)
return null;
const inputToolbarProps = {
...props,
text: getTextFromProp(text),
composerHeight: Math.max(minComposerHeight, composerHeight),
onSend: _onSend,
onInputSizeChanged,
onTextChanged: _onInputTextChanged,
textInputProps: {
...textInputProps,
ref: textInputRef,
maxLength: isTypingDisabled ? 0 : maxInputLength,
},
};
if (renderInputToolbar)
return renderInputToolbar(inputToolbarProps);
return <InputToolbar {...inputToolbarProps}/>;
}, [
isInitialized,
_onSend,
getTextFromProp,
maxInputLength,
minComposerHeight,
onInputSizeChanged,
props,
text,
renderInputToolbar,
composerHeight,
isTypingDisabled,
textInputRef,
textInputProps,
_onInputTextChanged,
]);
const contextValues = useMemo(() => ({
actionSheet: actionSheet ||
(() => ({
showActionSheetWithOptions: actionSheetRef.current.showActionSheetWithOptions,
})),
getLocale: () => locale,
}), [actionSheet, locale]);
useEffect(() => {
if (props.text != null)
setText(props.text);
}, [props.text]);
useAnimatedReaction(() => -keyboard.height.value, (value, prevValue) => {
if (prevValue !== null && value !== prevValue) {
const isKeyboardMovingUp = value > prevValue;
if (isKeyboardMovingUp !== trackingKeyboardMovement.value) {
trackingKeyboardMovement.value = isKeyboardMovingUp;
keyboardOffsetBottom.value = withTiming(isKeyboardMovingUp ? bottomOffset : 0, {
// If `bottomOffset` exists, we change the duration to a smaller value to fix the delay in the keyboard animation speed
duration: bottomOffset ? 150 : 400,
});
if (focusOnInputWhenOpeningKeyboard)
if (isKeyboardMovingUp)
runOnJS(handleTextInputFocusWhenKeyboardShow)();
else
runOnJS(handleTextInputFocusWhenKeyboardHide)();
if (value === 0) {
runOnJS(enableTyping)();
}
else {
runOnJS(disableTyping)();
runOnJS(debounceEnableTyping)();
}
}
}
}, [
keyboard,
trackingKeyboardMovement,
focusOnInputWhenOpeningKeyboard,
handleTextInputFocusWhenKeyboardHide,
handleTextInputFocusWhenKeyboardShow,
enableTyping,
disableTyping,
debounceEnableTyping,
bottomOffset,
]);
return (<GiftedChatContext.Provider value={contextValues}>
<ActionSheetProvider ref={actionSheetRef}>
<View testID={TEST_ID.WRAPPER} style={[stylesCommon.fill, styles.contentContainer]} onLayout={onInitialLayoutViewLayout}>
{isInitialized
? (<Animated.View style={[stylesCommon.fill, isKeyboardInternallyHandled && contentStyleAnim]}>
{renderMessages}
{inputToolbarFragment}
</Animated.View>)
: (renderLoading?.())}
</View>
</ActionSheetProvider>
</GiftedChatContext.Provider>);
}
function GiftedChatWrapper(props) {
return (<KeyboardProvider>
<GiftedChat {...props}/>
</KeyboardProvider>);
}
GiftedChatWrapper.append = (currentMessages = [], messages, inverted = true) => {
if (!Array.isArray(messages))
messages = [messages];
return inverted
? messages.concat(currentMessages)
: currentMessages.concat(messages);
};
GiftedChatWrapper.prepend = (currentMessages = [], messages, inverted = true) => {
if (!Array.isArray(messages))
messages = [messages];
return inverted
? currentMessages.concat(messages)
: messages.concat(currentMessages);
};
export * from '../types';
export { GiftedChatWrapper as GiftedChat, Actions, Avatar, Bubble, SystemMessage, MessageImage, MessageText, Composer, Day, InputToolbar, LoadEarlier, Message, MessageContainer, Send, Time, GiftedAvatar, utils };
//# sourceMappingURL=index.js.map