UNPKG

react-native-gifted-chat

Version:
258 lines 13.5 kB
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { View, } from 'react-native'; import { Pressable, Text } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { LoadEarlierMessages } from '../LoadEarlierMessages'; import { warning } from '../logging'; import stylesCommon from '../styles'; import { TypingIndicator } from '../TypingIndicator'; import { isSameDay, useCallbackThrottled } from '../utils'; import { DayAnimated } from './components/DayAnimated'; import { Item } from './components/Item'; import styles from './styles'; import { AnimatedFlatList } from './types'; export * from './types'; export const MessagesContainer = (props) => { const { messages = [], user, isTyping = false, renderChatEmpty: renderChatEmptyProp, isInverted = true, listProps, isScrollToBottomEnabled = false, scrollToBottomOffset = 200, isAlignedTop = false, scrollToBottomStyle, scrollToBottomContentStyle, loadEarlierMessagesProps, renderTypingIndicator: renderTypingIndicatorProp, renderFooter: renderFooterProp, renderLoadEarlier: renderLoadEarlierProp, forwardRef, scrollToBottomComponent: scrollToBottomComponentProp, renderDay: renderDayProp, isDayAnimationEnabled = true, } = props; const listPropsOnScrollProp = listProps?.onScroll; const scrollToBottomOpacity = useSharedValue(0); const isScrollingDown = useSharedValue(false); const lastScrolledY = useSharedValue(0); const [isScrollToBottomVisible, setIsScrollToBottomVisible] = useState(false); const scrollToBottomStyleAnim = useAnimatedStyle(() => ({ opacity: scrollToBottomOpacity.value, }), [scrollToBottomOpacity]); const daysPositions = useSharedValue({}); const listHeight = useSharedValue(0); const scrolledY = useSharedValue(0); const renderTypingIndicator = useCallback(() => { if (renderTypingIndicatorProp) return renderTypingIndicatorProp(); return <TypingIndicator isTyping={isTyping} style={props.typingIndicatorStyle}/>; }, [isTyping, renderTypingIndicatorProp, props.typingIndicatorStyle]); const ListFooterComponent = useMemo(() => { if (renderFooterProp) return renderFooterProp(props); return renderTypingIndicator(); }, [renderFooterProp, renderTypingIndicator, props]); const renderLoadEarlier = useCallback(() => { if (loadEarlierMessagesProps?.isAvailable) { if (renderLoadEarlierProp) return renderLoadEarlierProp(loadEarlierMessagesProps); return <LoadEarlierMessages {...loadEarlierMessagesProps}/>; } return null; }, [loadEarlierMessagesProps, renderLoadEarlierProp]); const changeScrollToBottomVisibility = useCallbackThrottled((isVisible) => { if (isScrollingDown.value && isVisible) return; if (isVisible) setIsScrollToBottomVisible(true); scrollToBottomOpacity.value = withTiming(isVisible ? 1 : 0, { duration: 250 }, isFinished => { if (isFinished && !isVisible) runOnJS(setIsScrollToBottomVisible)(false); }); }, [scrollToBottomOpacity, isScrollingDown], 50); const scrollTo = useCallback((options) => { if (options) forwardRef?.current?.scrollToOffset(options); }, [forwardRef]); const doScrollToBottom = useCallback((animated = true) => { isScrollingDown.value = true; changeScrollToBottomVisibility(false); if (isInverted) scrollTo({ offset: 0, animated }); else if (forwardRef?.current) forwardRef.current.scrollToEnd({ animated }); }, [forwardRef, isInverted, scrollTo, isScrollingDown, changeScrollToBottomVisibility]); const handleOnScroll = useCallback((event) => { listPropsOnScrollProp?.(event); const { contentOffset: { y: contentOffsetY }, contentSize: { height: contentSizeHeight }, layoutMeasurement: { height: layoutMeasurementHeight }, } = event; isScrollingDown.value = (isInverted && lastScrolledY.value > contentOffsetY) || (!isInverted && lastScrolledY.value < contentOffsetY); lastScrolledY.value = contentOffsetY; if (isInverted) if (contentOffsetY > scrollToBottomOffset) changeScrollToBottomVisibility(true); else changeScrollToBottomVisibility(false); else if (contentOffsetY < scrollToBottomOffset && contentSizeHeight - layoutMeasurementHeight > scrollToBottomOffset) changeScrollToBottomVisibility(false); else changeScrollToBottomVisibility(false); }, [isInverted, scrollToBottomOffset, changeScrollToBottomVisibility, isScrollingDown, lastScrolledY, listPropsOnScrollProp]); const restProps = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { messages: _, ...rest } = props; return rest; }, [props]); const renderItem = useCallback(({ item, index }) => { const messageItem = item; if (!messageItem._id && messageItem._id !== 0) warning('GiftedChat: `_id` is missing for message', JSON.stringify(item)); if (!messageItem.user) { if (!messageItem.system) warning('GiftedChat: `user` is missing for message', JSON.stringify(messageItem)); messageItem.user = { _id: 0 }; } if (messages) { const previousMessage = (isInverted ? messages[index + 1] : messages[index - 1]) || {}; const nextMessage = (isInverted ? messages[index - 1] : messages[index + 1]) || {}; const messageProps = { position: user?._id != null && messageItem.user?._id === user._id ? 'right' : 'left', ...restProps, currentMessage: messageItem, previousMessage, nextMessage, scrolledY, daysPositions, listHeight, isDayAnimationEnabled, }; return (<Item {...messageProps}/>); } return null; }, [messages, restProps, isInverted, scrolledY, daysPositions, listHeight, isDayAnimationEnabled, user]); const emptyContent = useMemo(() => { if (!renderChatEmptyProp) return null; return renderChatEmptyProp(); }, [renderChatEmptyProp]); const renderChatEmpty = useCallback(() => { if (renderChatEmptyProp) return isInverted ? (emptyContent) : (<View style={[stylesCommon.fill, styles.emptyChatContainer]}> {emptyContent} </View>); return <View style={stylesCommon.fill}/>; }, [isInverted, renderChatEmptyProp, emptyContent]); const ListHeaderComponent = useMemo(() => { const content = renderLoadEarlier(); if (!content) return null; return (<View style={stylesCommon.fill}>{content}</View>); }, [renderLoadEarlier]); const renderScrollBottomComponent = useCallback(() => { if (scrollToBottomComponentProp) return scrollToBottomComponentProp(); return <Text>{'V'}</Text>; }, [scrollToBottomComponentProp]); const handleScrollToBottomPress = useCallback(() => { doScrollToBottom(); }, [doScrollToBottom]); const scrollToBottomContent = useMemo(() => { return (<Animated.View style={[ stylesCommon.centerItems, styles.scrollToBottomContent, scrollToBottomContentStyle, scrollToBottomStyleAnim, ]}> {renderScrollBottomComponent()} </Animated.View>); }, [scrollToBottomStyleAnim, scrollToBottomContentStyle, renderScrollBottomComponent]); const ScrollToBottomWrapper = useCallback(() => { if (!isScrollToBottomEnabled) return null; if (!isScrollToBottomVisible) return null; return (<Pressable style={[styles.scrollToBottom, scrollToBottomStyle]} onPress={handleScrollToBottomPress}> {scrollToBottomContent} </Pressable>); }, [isScrollToBottomEnabled, isScrollToBottomVisible, handleScrollToBottomPress, scrollToBottomContent, scrollToBottomStyle]); const onLayoutList = useCallback((event) => { listHeight.value = event.nativeEvent.layout.height; if (!isInverted && messages?.length && isScrollToBottomEnabled) setTimeout(() => { doScrollToBottom(false); }, 500); // listProps.onLayout may be a SharedValue in Reanimated types, but we only accept functions const onLayoutProp = listProps?.onLayout; onLayoutProp?.(event); }, [isInverted, messages, doScrollToBottom, listHeight, listProps, isScrollToBottomEnabled]); const onEndReached = useCallback(() => { if (loadEarlierMessagesProps && loadEarlierMessagesProps.isAvailable && loadEarlierMessagesProps.isInfiniteScrollEnabled && !loadEarlierMessagesProps.isLoading) loadEarlierMessagesProps.onPress(); }, [loadEarlierMessagesProps]); const keyExtractor = useCallback((item) => item._id.toString(), []); const renderCell = useCallback((props) => { const { item, onLayout: onLayoutProp, children } = props; const id = item._id.toString(); const handleOnLayout = (event) => { onLayoutProp?.(event); // Only track positions when day animation is enabled if (!isDayAnimationEnabled) return; const { y, height } = event.nativeEvent.layout; const newValue = { y, height, createdAt: new Date(item.createdAt).getTime(), }; daysPositions.modify(value => { 'worklet'; const isSameDay = (date1, date2) => { const d1 = new Date(date1); const d2 = new Date(date2); return (d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear()); }; for (const [key, item] of Object.entries(value)) if (isSameDay(newValue.createdAt, item.createdAt) && (isInverted ? item.y <= newValue.y : item.y >= newValue.y)) { delete value[key]; break; } // @ts-expect-error: https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks value[id] = newValue; return value; }); }; return (<View {...props} onLayout={handleOnLayout}> {children} </View>); }, [daysPositions, isInverted, isDayAnimationEnabled]); const scrollHandler = useAnimatedScrollHandler({ onScroll: event => { scrolledY.value = event.contentOffset.y; runOnJS(handleOnScroll)(event); }, }, [handleOnScroll]); // removes unrendered days positions when messages are added/removed useEffect(() => { // Skip cleanup when day animation is disabled if (!isDayAnimationEnabled) return; Object.keys(daysPositions.value).forEach(key => { const messageIndex = messages.findIndex(m => m._id.toString() === key); let shouldRemove = messageIndex === -1; if (!shouldRemove) { const prevMessage = messages[messageIndex + (isInverted ? 1 : -1)]; const message = messages[messageIndex]; shouldRemove = !!prevMessage && isSameDay(message, prevMessage); } if (shouldRemove) daysPositions.modify(value => { 'worklet'; delete value[key]; return value; }); }); }, [messages, daysPositions, isInverted, isDayAnimationEnabled]); return (<View style={[ styles.contentContainerStyle, isAlignedTop ? styles.containerAlignTop : stylesCommon.fill, ]}> <AnimatedFlatList ref={forwardRef} keyExtractor={keyExtractor} data={messages} renderItem={renderItem} inverted={isInverted} automaticallyAdjustContentInsets={false} style={stylesCommon.fill} contentContainerStyle={styles.messagesContainer} ListEmptyComponent={renderChatEmpty} ListFooterComponent={isInverted ? ListHeaderComponent : <>{ListFooterComponent}</>} ListHeaderComponent={isInverted ? <>{ListFooterComponent}</> : ListHeaderComponent} scrollEventThrottle={1} onEndReached={onEndReached} onEndReachedThreshold={0.1} keyboardDismissMode='interactive' keyboardShouldPersistTaps='handled' {...listProps} onScroll={scrollHandler} onLayout={onLayoutList} CellRendererComponent={renderCell}/> <ScrollToBottomWrapper /> {isDayAnimationEnabled && (<DayAnimated scrolledY={scrolledY} daysPositions={daysPositions} listHeight={listHeight} renderDay={renderDayProp} messages={messages} isLoading={loadEarlierMessagesProps?.isLoading ?? false} dateFormat={props.dateFormat} dateFormatCalendar={props.dateFormatCalendar}/>)} </View>); }; //# sourceMappingURL=index.js.map