From bb62c09d26318608139df344b95848c4caad5204 Mon Sep 17 00:00:00 2001 From: Oleksii Kozulin Date: Fri, 15 Sep 2023 16:35:47 +0300 Subject: [PATCH] Fixed issue with bad isInitialized flag. Optimised component with useCallback. --- package.json | 2 +- src/GiftedChat.tsx | 467 ++++++++++++++++++++++++--------------------- 2 files changed, 246 insertions(+), 223 deletions(-) diff --git a/package.json b/package.json index b25c3c8a5..3126d3086 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-gifted-chat", - "version": "2.4.0", + "version": "2.5.0", "description": "The most complete chat UI for React Native", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/GiftedChat.tsx b/src/GiftedChat.tsx index eaa351ae6..46a32189d 100644 --- a/src/GiftedChat.tsx +++ b/src/GiftedChat.tsx @@ -6,7 +6,14 @@ import { import dayjs from 'dayjs' import localizedFormat from 'dayjs/plugin/localizedFormat' import PropTypes from 'prop-types' -import React, { createRef, useEffect, useMemo, useRef, useState } from 'react' +import React, { + createRef, + useEffect, + useMemo, + useRef, + useState, + useCallback, +} from 'react' import { Animated, FlatList, @@ -273,24 +280,20 @@ function GiftedChat( let _isTextInputWasFocused: boolean = false - const [state, setState] = useState({ - isInitialized: false, // initialization will calculate maxHeight before rendering the chat - composerHeight: minComposerHeight, - messagesContainerHeight: undefined, - typingDisabled: false, - text: undefined, - messages: undefined, - }) + const [isInitialized, setIsInitialized] = useState(false) + const [composerHeight, setComposerHeight] = useState(minComposerHeight) + const [messagesContainerHeight, setMessagesContainerHeight] = useState< + number + >() + const [typingDisabled, setTypingDisabled] = useState(false) + const [textState, setTextState] = useState(text) useEffect(() => { isMountedRef.current = true - setState({ - ...state, - messages, - // Text prop takes precedence over state. - ...(text !== undefined && text !== state.text && { text: text }), - }) + if (text !== undefined && text !== textState) { + setTextState(text) + } if (inverted === false && messages?.length) { setTimeout(() => scrollToBottom(false), 200) @@ -299,17 +302,20 @@ function GiftedChat( return () => { isMountedRef.current = false } - }, [messages, text]) + }, [messages?.length, text]) - const getTextFromProp = (fallback: string) => { - if (text === undefined) { - return fallback - } + const getTextFromProp = useCallback( + (fallback: string) => { + if (text === undefined) { + return fallback + } - return text - } + return text + }, + [text], + ) - const getKeyboardHeight = () => { + const getKeyboardHeight = useCallback(() => { if (Platform.OS === 'android' && !forceGetKeyboardHeight) { // For android: on-screen keyboard resized main container and has own height. // @see https://developer.android.com/training/keyboard-input/visibility.html @@ -318,37 +324,45 @@ function GiftedChat( } return keyboardHeightRef.current - } + }, [forceGetKeyboardHeight]) - const calculateInputToolbarHeight = (composerHeight: number) => { - const getMinInputToolbarHeight = renderAccessory - ? minInputToolbarHeight! * 2 - : minInputToolbarHeight + const calculateInputToolbarHeight = useCallback( + (composerHeight: number) => { + const getMinInputToolbarHeight = renderAccessory + ? minInputToolbarHeight! * 2 + : minInputToolbarHeight - return composerHeight + (getMinInputToolbarHeight! - minComposerHeight!) - } + return composerHeight + (getMinInputToolbarHeight! - minComposerHeight!) + }, + [minInputToolbarHeight, minComposerHeight], + ) /** * Returns the height, based on current window size, without taking the keyboard into account. */ - const getBasicMessagesContainerHeight = ( - composerHeight = state.composerHeight, - ) => { - return maxHeightRef.current! - calculateInputToolbarHeight(composerHeight!) - } + const getBasicMessagesContainerHeight = useCallback( + (composerHeightParameter = composerHeight) => { + return ( + maxHeightRef.current! - + calculateInputToolbarHeight(composerHeightParameter!) + ) + }, + [composerHeight], + ) /** * Returns the height, based on current window size, taking the keyboard into account. */ - const getMessagesContainerHeightWithKeyboard = ( - composerHeight = state.composerHeight, - ) => { - return ( - getBasicMessagesContainerHeight(composerHeight) - - getKeyboardHeight() + - bottomOffsetRef.current - ) - } + const getMessagesContainerHeightWithKeyboard = useCallback( + (composerHeightParameter = composerHeight) => { + return ( + getBasicMessagesContainerHeight(composerHeightParameter) - + getKeyboardHeight() + + bottomOffsetRef.current + ) + }, + [composerHeight], + ) /** * Store text input focus status when keyboard hide to retrieve @@ -356,17 +370,17 @@ function GiftedChat( * `onKeyboardWillHide` may be called twice in sequence so we * make a guard condition (eg. showing image picker) */ - const handleTextInputFocusWhenKeyboardHide = () => { + const handleTextInputFocusWhenKeyboardHide = useCallback(() => { if (!_isTextInputWasFocused) { _isTextInputWasFocused = textInputRef.current?.isFocused() || false } - } + }, [_isTextInputWasFocused, 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 = () => { + const handleTextInputFocusWhenKeyboardShow = useCallback(() => { if ( textInputRef.current && _isTextInputWasFocused && @@ -377,79 +391,93 @@ function GiftedChat( // Reset the indicator since the keyboard is shown _isTextInputWasFocused = false - } + }, [_isTextInputWasFocused, textInputRef]) - const onKeyboardWillShow = (e: any) => { - handleTextInputFocusWhenKeyboardShow() + const onKeyboardWillShow = useCallback( + (e: any) => { + handleTextInputFocusWhenKeyboardShow() - if (isKeyboardInternallyHandled) { - keyboardHeightRef.current = e.endCoordinates - ? e.endCoordinates.height - : e.end.height + if (isKeyboardInternallyHandled) { + keyboardHeightRef.current = e.endCoordinates + ? e.endCoordinates.height + : e.end.height - bottomOffsetRef.current = bottomOffset != null ? bottomOffset : 1 + bottomOffsetRef.current = bottomOffset != null ? bottomOffset : 1 - const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard() - - setState({ - ...state, - typingDisabled: true, - messagesContainerHeight: newMessagesContainerHeight, - }) - } - } + const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard() + setTypingDisabled(true) + setMessagesContainerHeight(newMessagesContainerHeight) + } + }, + [ + handleTextInputFocusWhenKeyboardShow, + isKeyboardInternallyHandled, + keyboardHeightRef, + bottomOffsetRef, + bottomOffset, + ], + ) - const onKeyboardWillHide = (_e: any) => { - handleTextInputFocusWhenKeyboardHide() + const onKeyboardWillHide = useCallback( + (_e: any) => { + handleTextInputFocusWhenKeyboardHide() - if (isKeyboardInternallyHandled) { - keyboardHeightRef.current = 0 - bottomOffsetRef.current = 0 + if (isKeyboardInternallyHandled) { + keyboardHeightRef.current = 0 + bottomOffsetRef.current = 0 - const newMessagesContainerHeight = getBasicMessagesContainerHeight() + const newMessagesContainerHeight = getBasicMessagesContainerHeight() - setState({ - ...state, - typingDisabled: true, - messagesContainerHeight: newMessagesContainerHeight, - }) - } - } + setTypingDisabled(true) + setMessagesContainerHeight(newMessagesContainerHeight) + } + }, + [ + isKeyboardInternallyHandled, + keyboardHeightRef, + bottomOffsetRef, + getBasicMessagesContainerHeight, + handleTextInputFocusWhenKeyboardHide, + ], + ) - const onKeyboardDidShow = (e: any) => { - if (Platform.OS === 'android') { - onKeyboardWillShow(e) - } + const onKeyboardDidShow = useCallback( + (e: any) => { + if (Platform.OS === 'android') { + onKeyboardWillShow(e) + } - setState({ - ...state, - typingDisabled: false, - }) - } + setTypingDisabled(false) + }, + [onKeyboardWillShow], + ) - const onKeyboardDidHide = (e: any) => { - if (Platform.OS === 'android') { - onKeyboardWillHide(e) - } + const onKeyboardDidHide = useCallback( + (e: any) => { + if (Platform.OS === 'android') { + onKeyboardWillHide(e) + } - setState({ - ...state, - typingDisabled: false, - }) - } + setTypingDisabled(false) + }, + [onKeyboardWillHide], + ) - const scrollToBottom = (animated = true) => { - if (messageContainerRef?.current) { - if (!inverted) { - messageContainerRef.current.scrollToEnd({ animated }) - } else { - messageContainerRef.current.scrollToOffset({ - offset: 0, - animated, - }) + const scrollToBottom = useCallback( + (animated = true) => { + if (messageContainerRef?.current) { + if (!inverted) { + messageContainerRef.current.scrollToEnd({ animated }) + } else { + messageContainerRef.current.scrollToOffset({ + offset: 0, + animated, + }) + } } - } - } + }, + [messageContainerRef, inverted], + ) const renderMessages = () => { const { messagesContainerStyle, ...messagesContainerProps } = props @@ -457,8 +485,8 @@ function GiftedChat( const fragment = ( ( onKeyboardDidShow: onKeyboardDidShow, onKeyboardDidHide: onKeyboardDidHide, }} - messages={state.messages} + messages={messages} forwardRef={messageContainerRef} isTyping={isTyping} /> @@ -488,49 +516,7 @@ function GiftedChat( ) } - const _onSend = ( - messages: TMessage[] = [], - shouldResetInputToolbar = false, - ) => { - if (!Array.isArray(messages)) { - messages = [messages] - } - - const newMessages: TMessage[] = messages.map(message => { - return { - ...message, - user: user!, - createdAt: new Date(), - _id: messageIdGenerator && messageIdGenerator(), - } - }) - - if (shouldResetInputToolbar === true) { - setState({ - ...state, - typingDisabled: true, - }) - - resetInputToolbar() - } - - if (onSend) { - onSend(newMessages) - } - - // if (shouldResetInputToolbar === true) { - // setTimeout(() => { - // if (isMountedRef.current === true) { - // setState({ - // ...state, - // typingDisabled: false, - // }) - // } - // }, 100) - // } - } - - const resetInputToolbar = () => { + const resetInputToolbar = useCallback(() => { if (textInputRef.current) { textInputRef.current.clear() } @@ -541,33 +527,63 @@ function GiftedChat( minComposerHeight, ) - setState({ - ...state, - text: getTextFromProp(''), - composerHeight: minComposerHeight, - messagesContainerHeight: newMessagesContainerHeight, - }) - } + setTextState(getTextFromProp('')) + setComposerHeight(minComposerHeight) + setMessagesContainerHeight(newMessagesContainerHeight) + }, [ + minComposerHeight, + getMessagesContainerHeightWithKeyboard, + textInputRef, + getTextFromProp, + ]) + + const _onSend = useCallback( + (messages: TMessage[] = [], shouldResetInputToolbar = false) => { + if (!Array.isArray(messages)) { + messages = [messages] + } - const onInputSizeChanged = (size: { height: number }) => { - const newComposerHeight = Math.max( - minComposerHeight!, - Math.min(maxComposerHeight!, size.height), - ) + const newMessages: TMessage[] = messages.map(message => { + return { + ...message, + user: user!, + createdAt: new Date(), + _id: messageIdGenerator && messageIdGenerator(), + } + }) - const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard( - newComposerHeight, - ) + if (shouldResetInputToolbar === true) { + setTypingDisabled(true) - setState({ - ...state, - composerHeight: newComposerHeight, - messagesContainerHeight: newMessagesContainerHeight, - }) - } + resetInputToolbar() + } - const _onInputTextChanged = (_text: string) => { - if (state.typingDisabled) { + if (onSend) { + onSend(newMessages) + } + }, + [resetInputToolbar, messageIdGenerator, onSend], + ) + + const onInputSizeChanged = useCallback( + (size: { height: number }) => { + const newComposerHeight = Math.max( + minComposerHeight!, + Math.min(maxComposerHeight!, size.height), + ) + + const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard( + newComposerHeight, + ) + + setComposerHeight(newComposerHeight) + setMessagesContainerHeight(newMessagesContainerHeight) + }, + [maxComposerHeight, minComposerHeight], + ) + + const _onInputTextChanged = useCallback((_text: string) => { + if (typingDisabled) { return } @@ -577,76 +593,83 @@ function GiftedChat( // Only set state if it's not being overridden by a prop. if (text === undefined) { - setState({ ...state, text: _text }) + setTextState(_text) } - } + }, []) - const notifyInputTextReset = () => { + const notifyInputTextReset = useCallback(() => { if (onInputTextChanged) { onInputTextChanged('') } - } + }, [onInputTextChanged]) - const onInitialLayoutViewLayout = (e: any) => { - const { layout } = e.nativeEvent + const onInitialLayoutViewLayout = useCallback( + (e: any) => { + const { layout } = e.nativeEvent - if (layout.height <= 0) { - return - } + if (layout.height <= 0) { + return + } - notifyInputTextReset() + notifyInputTextReset() - maxHeightRef.current = layout.height + maxHeightRef.current = layout.height - const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard( - minComposerHeight, - ) + const newMessagesContainerHeight = getMessagesContainerHeightWithKeyboard( + minComposerHeight, + ) - setState({ - ...state, - isInitialized: true, - text: getTextFromProp(initialText), - composerHeight: minComposerHeight, - messagesContainerHeight: newMessagesContainerHeight, - }) - } + setIsInitialized(true) + setTextState(getTextFromProp(initialText)) + setComposerHeight(minComposerHeight) + setMessagesContainerHeight(newMessagesContainerHeight) + }, + [initialText, minComposerHeight, maxHeightRef, notifyInputTextReset], + ) - const onMainViewLayout = (e: LayoutChangeEvent) => { - // TODO: fix an issue when keyboard is dismissing during the initialization - const { layout } = e.nativeEvent + const onMainViewLayout = useCallback( + (e: LayoutChangeEvent) => { + // TODO: fix an issue when keyboard is dismissing during the initialization + const { layout } = e.nativeEvent - if ( - maxHeightRef.current !== layout.height || - isFirstLayoutRef.current === true - ) { - maxHeightRef.current = layout.height + if ( + maxHeightRef.current !== layout.height || + isFirstLayoutRef.current === true + ) { + maxHeightRef.current = layout.height - setState({ - ...state, - messagesContainerHeight: + setMessagesContainerHeight( keyboardHeightRef.current > 0 ? getMessagesContainerHeightWithKeyboard() : getBasicMessagesContainerHeight(), - }) - } + ) + } - if (isFirstLayoutRef.current === true) { - isFirstLayoutRef.current = false - } - } + if (isFirstLayoutRef.current === true) { + isFirstLayoutRef.current = false + } + }, + [ + maxHeightRef, + isFirstLayoutRef, + keyboardHeightRef, + getMessagesContainerHeightWithKeyboard, + getBasicMessagesContainerHeight, + ], + ) const _renderInputToolbar = () => { const inputToolbarProps = { ...props, - text: getTextFromProp(state.text!), - composerHeight: Math.max(minComposerHeight!, state.composerHeight!), + text: getTextFromProp(textState!), + composerHeight: Math.max(minComposerHeight!, composerHeight!), onSend: _onSend, onInputSizeChanged: onInputSizeChanged, onTextChanged: _onInputTextChanged, textInputProps: { ...textInputProps, ref: textInputRef, - maxLength: state.typingDisabled ? 0 : maxInputLength, + maxLength: typingDisabled ? 0 : maxInputLength, }, } @@ -657,21 +680,21 @@ function GiftedChat( return } - const _renderChatFooter = () => { + const _renderChatFooter = useCallback(() => { if (renderChatFooter) { return renderChatFooter() } return null - } + }, [renderChatFooter]) - const _renderLoading = () => { + const _renderLoading = useCallback(() => { if (renderLoading) { return renderLoading() } return null - } + }, [renderLoading]) const contextValues = useMemo( () => ({ @@ -681,7 +704,7 @@ function GiftedChat( [actionSheet, locale], ) - if (state.isInitialized === true) { + if (isInitialized === true) { return (