UNPKG

react-native-gifted-chat-flashlist

Version:

React Native Gifted Chat with FlashList optimization for better performance

301 lines 12.4 kB
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, StyleSheet, View, } from 'react-native'; import { v4 as uuidv4 } from 'uuid'; 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, { useAnimatedKeyboard, useAnimatedStyle, useAnimatedReaction, useSharedValue, withTiming, runOnJS, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; dayjs.extend(localizedFormat); function GiftedChat(props) { const { messages = [], initialText = '', isTyping, messageIdGenerator = () => uuidv4(), user = {}, onSend, locale = 'en', renderLoading, actionSheet = null, textInputProps, renderChatFooter = null, renderInputToolbar = null, bottomOffset = 0, keyboardShouldPersistTaps = Platform.select({ ios: 'never', android: 'always', default: 'never', }), onInputTextChanged = null, maxInputLength = null, inverted = true, minComposerHeight = MIN_COMPOSER_HEIGHT, maxComposerHeight = MAX_COMPOSER_HEIGHT, } = 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 = useAnimatedKeyboard(); const trackingKeyboardMovement = useSharedValue(false); const debounceEnableTypingTimeoutId = useRef(); const insets = useSafeAreaInsets(); 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 && !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 = true) => { if (!messageContainerRef?.current) return; if (inverted) { messageContainerRef.current.scrollToOffset({ offset: 0, animated: isAnimated, }); return; } messageContainerRef.current.scrollToEnd({ animated: isAnimated }); }, [inverted, messageContainerRef]); const renderMessages = useMemo(() => { if (!isInitialized) return null; const { messagesContainerStyle, ...messagesContainerProps } = props; const fragment = (<View style={[styles.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); }, [messageIdGenerator, onSend, user, resetInputToolbar, disableTyping]); 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) => { const { layout } = e.nativeEvent; if (layout.height <= 0) return; notifyInputTextReset(); setIsInitialized(true); setComposerHeight(minComposerHeight); setText(getTextFromProp(initialText)); }, [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]); useEffect(() => { if (!inverted && messages?.length) setTimeout(() => scrollToBottom(false), 200); }, [messages?.length, inverted, scrollToBottom]); useAnimatedReaction(() => keyboard.height.value, (value, prevValue) => { if (prevValue && value !== prevValue) { const isKeyboardMovingUp = value > prevValue; if (isKeyboardMovingUp !== trackingKeyboardMovement.value) { trackingKeyboardMovement.value = isKeyboardMovingUp; keyboardOffsetBottom.value = withTiming(isKeyboardMovingUp ? insets.bottom + 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 (isKeyboardMovingUp) runOnJS(handleTextInputFocusWhenKeyboardShow)(); else runOnJS(handleTextInputFocusWhenKeyboardHide)(); if (value === 0) { runOnJS(enableTyping)(); } else { runOnJS(disableTyping)(); runOnJS(debounceEnableTyping)(); } } } }, [ keyboard, trackingKeyboardMovement, insets, handleTextInputFocusWhenKeyboardHide, handleTextInputFocusWhenKeyboardShow, enableTyping, disableTyping, debounceEnableTyping, ]); return (<GiftedChatContext.Provider value={contextValues}> <ActionSheetProvider ref={actionSheetRef}> <View testID={TEST_ID.WRAPPER} style={[styles.fill, styles.contentContainer]} onLayout={onInitialLayoutViewLayout}> {isInitialized ? (<Animated.View style={[styles.fill, contentStyleAnim]}> {renderMessages} {inputToolbarFragment} </Animated.View>) : (renderLoading?.())} </View> </ActionSheetProvider> </GiftedChatContext.Provider>); } GiftedChat.append = (currentMessages = [], messages, inverted = true) => { if (!Array.isArray(messages)) messages = [messages]; return inverted ? messages.concat(currentMessages) : currentMessages.concat(messages); }; GiftedChat.prepend = (currentMessages = [], messages, inverted = true) => { if (!Array.isArray(messages)) messages = [messages]; return inverted ? currentMessages.concat(messages) : messages.concat(currentMessages); }; const styles = StyleSheet.create({ fill: { flex: 1, }, contentContainer: { overflow: 'hidden', }, }); export * from './Models'; export { GiftedChat, Actions, Avatar, Bubble, SystemMessage, MessageImage, MessageText, Composer, Day, InputToolbar, LoadEarlier, Message, MessageContainer, Send, Time, GiftedAvatar, utils }; //# sourceMappingURL=GiftedChat.js.map