@qte/react-native-gifted-chat
Version:
Performant fork of react-native-gifted-chat with FlashList support
205 lines • 10.2 kB
JavaScript
import React, { useCallback, useMemo, useState } from 'react';
import { View, TouchableOpacity, Text, Platform, } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import DayAnimated from './components/DayAnimated';
import Item from './components/Item';
import { LoadEarlier } from '../LoadEarlier';
import TypingIndicator from '../TypingIndicator';
import { warning } from '../logging';
import stylesCommon from '../styles';
import styles from './styles';
export * from './types';
function MessageContainer(props) {
const { messages: messagesProp = [], user, isTyping = false, renderChatEmpty: renderChatEmptyProp, onLoadEarlier, inverted = true, loadEarlier = false, listViewProps, invertibleScrollViewProps, extraData = null, isScrollToBottomEnabled = false, scrollToBottomOffset = 200, alignTop = false, scrollToBottomStyle, infiniteScroll = false, isLoadingEarlier = false, renderTypingIndicator: renderTypingIndicatorProp, renderFooter: renderFooterProp, renderLoadEarlier: renderLoadEarlierProp, forwardRef, handleOnScroll: handleOnScrollProp, scrollToBottomComponent: scrollToBottomComponentProp, dayAnimated = true, } = props;
const messages = useMemo(() => inverted ? [...messagesProp].reverse() : messagesProp, [inverted, messagesProp]);
const scrollToBottomOpacity = 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}/>;
}, [isTyping, renderTypingIndicatorProp]);
const ListFooterComponent = useMemo(() => {
if (renderFooterProp)
return <>{renderFooterProp(props)}</>;
return <>{renderTypingIndicator()}</>;
}, [renderFooterProp, renderTypingIndicator, props]);
const renderLoadEarlier = useCallback(() => {
if (loadEarlier === true) {
if (renderLoadEarlierProp)
return renderLoadEarlierProp(props);
return <LoadEarlier {...props}/>;
}
return null;
}, [loadEarlier, renderLoadEarlierProp, props]);
const doScrollToBottom = useCallback((animated = false) => {
if (forwardRef?.current)
forwardRef.current.scrollToEnd({ animated });
}, [forwardRef, inverted]);
const handleOnScroll = useCallback((event) => {
handleOnScrollProp?.(event);
const { contentOffset: { y: contentOffsetY }, contentSize: { height: contentSizeHeight }, layoutMeasurement: { height: layoutMeasurementHeight }, } = event.nativeEvent;
const duration = 250;
const makeScrollToBottomVisible = () => {
setIsScrollToBottomVisible(true);
scrollToBottomOpacity.value = withTiming(1, { duration });
};
const makeScrollToBottomHidden = () => {
scrollToBottomOpacity.value = withTiming(0, { duration }, isFinished => {
if (isFinished)
runOnJS(setIsScrollToBottomVisible)(false);
});
};
if (inverted)
if (contentOffsetY > scrollToBottomOffset)
makeScrollToBottomVisible();
else
makeScrollToBottomHidden();
else if (contentOffsetY < scrollToBottomOffset &&
contentSizeHeight - layoutMeasurementHeight > scrollToBottomOffset)
makeScrollToBottomVisible();
else
makeScrollToBottomHidden();
}, [handleOnScrollProp, inverted, scrollToBottomOffset, scrollToBottomOpacity]);
const handleLayoutDayWrapper = useCallback((ref, id, createdAt) => {
if (!dayAnimated)
// If dayAnimated is false, we do not need to track day positions.
return;
setTimeout(() => {
const itemLayout = forwardRef?.current?.getLayout(messages.findIndex(m => m._id === id));
if (ref && itemLayout)
daysPositions.modify(value => {
'worklet';
// @ts-expect-error: https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks
value[id] = {
...itemLayout,
createdAt,
};
return value;
});
else if (daysPositions.value[id] != null)
daysPositions.modify(value => {
'worklet';
delete value[id];
return value;
});
}, 100);
}, [messages, daysPositions, forwardRef]);
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 };
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { messages: _, ...restProps } = props;
if (messages && user) {
const previousMessage = messages[index - 1] || {};
const nextMessage = messages[index + 1] || {};
const messageProps = {
...restProps,
currentMessage: messageItem,
previousMessage,
nextMessage,
position: messageItem.user._id === user._id ? 'right' : 'left',
onRefDayWrapper: handleLayoutDayWrapper,
scrolledY,
daysPositions,
listHeight,
};
return (<Item {...messageProps}/>);
}
return null;
}, [props, inverted, handleLayoutDayWrapper, scrolledY, daysPositions, listHeight, user]);
const renderChatEmpty = useCallback(() => {
if (renderChatEmptyProp)
return inverted
? (renderChatEmptyProp())
: (<View style={[stylesCommon.fill, styles.emptyChatContainer]}>
{renderChatEmptyProp()}
</View>);
return <View style={stylesCommon.fill}/>;
}, [inverted, renderChatEmptyProp]);
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 renderScrollToBottomWrapper = useCallback(() => {
if (!isScrollToBottomVisible)
return null;
return (<Animated.View style={[
stylesCommon.centerItems,
styles.scrollToBottomStyle,
scrollToBottomStyle,
scrollToBottomStyleAnim,
]}>
<TouchableOpacity onPress={() => doScrollToBottom()} hitSlop={{ top: 5, left: 5, right: 5, bottom: 5 }}>
{renderScrollBottomComponent()}
</TouchableOpacity>
</Animated.View>);
}, [scrollToBottomStyle, renderScrollBottomComponent, doScrollToBottom, scrollToBottomStyleAnim, isScrollToBottomVisible]);
const onLayoutList = useCallback((event) => {
listHeight.value = event.nativeEvent.layout.height;
if (!inverted &&
messages?.length)
setTimeout(() => {
doScrollToBottom(false);
}, 500);
listViewProps?.onLayout?.(event);
}, [inverted, messages, doScrollToBottom, listHeight, listViewProps]);
const onEndReached = useCallback(() => {
if (infiniteScroll &&
loadEarlier &&
onLoadEarlier &&
!isLoadingEarlier &&
Platform.OS !== 'web')
onLoadEarlier();
}, [infiniteScroll, loadEarlier, onLoadEarlier, isLoadingEarlier]);
const keyExtractor = useCallback((item) => item._id.toString(), []);
const onReachedProps = inverted
? {
onStartReached: onEndReached,
onStartReachedThreshold: 0.1,
}
: {
onEndReached,
onEndReachedThreshold: 0.1,
};
return (<View style={[
styles.contentContainerStyle,
alignTop ? styles.containerAlignTop : stylesCommon.fill,
]}>
<FlashList ref={forwardRef} extraData={[extraData, isTyping]} keyExtractor={keyExtractor} automaticallyAdjustContentInsets={false} data={messages} style={stylesCommon.fill} renderItem={renderItem} {...invertibleScrollViewProps} ListEmptyComponent={renderChatEmpty} ListFooterComponent={ListFooterComponent} ListHeaderComponent={ListHeaderComponent} onScroll={event => {
scrolledY.value = event.nativeEvent.contentOffset.y;
handleOnScroll(event);
}} scrollEventThrottle={16} {...onReachedProps} {...listViewProps} maintainVisibleContentPosition={{
autoscrollToBottomThreshold: 0.2,
animateAutoScrollToBottom: true,
...listViewProps?.maintainVisibleContentPosition,
startRenderingFromBottom: inverted,
}} onLayout={onLayoutList}/>
{isScrollToBottomEnabled
? renderScrollToBottomWrapper()
: null}
{dayAnimated && <DayAnimated scrolledY={scrolledY} daysPositions={daysPositions} listHeight={listHeight} messages={messages} isLoadingEarlier={isLoadingEarlier}/>}
</View>);
}
export default MessageContainer;
//# sourceMappingURL=index.js.map