react-native-gifted-chat
Version:
The most complete chat UI for React Native
224 lines • 11.2 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { View, TouchableOpacity, Text, Platform, FlatList, } from 'react-native';
import Animated, { runOnJS, useAnimatedScrollHandler, 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';
import { isSameDay } from '../utils';
export * from './types';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
function MessageContainer(props) {
const { messages = [], 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, } = props;
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) {
if (renderLoadEarlierProp)
return renderLoadEarlierProp(props);
return <LoadEarlier {...props}/>;
}
return null;
}, [loadEarlier, renderLoadEarlierProp, props]);
const scrollTo = useCallback((options) => {
if (forwardRef?.current && options)
forwardRef.current.scrollToOffset(options);
}, [forwardRef]);
const doScrollToBottom = useCallback((animated = true) => {
if (inverted)
scrollTo({ offset: 0, animated });
else if (forwardRef?.current)
forwardRef.current.scrollToEnd({ animated });
}, [forwardRef, inverted, scrollTo]);
const handleOnScroll = useCallback((event) => {
handleOnScrollProp?.(event);
const { contentOffset: { y: contentOffsetY }, contentSize: { height: contentSizeHeight }, layoutMeasurement: { height: layoutMeasurementHeight }, } = event;
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 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 };
}
const { messages, ...restProps } = props;
if (messages && user) {
const previousMessage = (inverted ? messages[index + 1] : messages[index - 1]) || {};
const nextMessage = (inverted ? messages[index - 1] : messages[index + 1]) || {};
const messageProps = {
...restProps,
currentMessage: messageItem,
previousMessage,
nextMessage,
position: messageItem.user._id === user._id ? 'right' : 'left',
scrolledY,
daysPositions,
listHeight,
};
return (<Item {...messageProps}/>);
}
return null;
}, [props, inverted, 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 (<TouchableOpacity onPress={() => doScrollToBottom()}>
<Animated.View style={[
stylesCommon.centerItems,
styles.scrollToBottomStyle,
scrollToBottomStyle,
scrollToBottomStyleAnim,
]}>
{renderScrollBottomComponent()}
</Animated.View>
</TouchableOpacity>);
}, [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 renderCell = useCallback((props) => {
const handleOnLayout = (event) => {
props.onLayout?.(event);
const { y, height } = event.nativeEvent.layout;
const newValue = {
y,
height,
createdAt: new Date(props.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) && (inverted ? 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[props.item._id] = newValue;
return value;
});
};
return (<View {...props} onLayout={handleOnLayout}>
{props.children}
</View>);
}, [daysPositions, inverted]);
const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
scrolledY.value = event.contentOffset.y;
runOnJS(handleOnScroll)(event);
},
}, [handleOnScroll]);
// removes unrendered days positions when messages are added/removed
useEffect(() => {
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 + (inverted ? 1 : -1)];
const message = messages[messageIndex];
shouldRemove = !!prevMessage && isSameDay(message, prevMessage);
}
if (shouldRemove)
daysPositions.modify(value => {
'worklet';
delete value[key];
return value;
});
});
}, [messages, daysPositions, inverted]);
return (<View style={[
styles.contentContainerStyle,
alignTop ? styles.containerAlignTop : stylesCommon.fill,
]}>
<AnimatedFlatList extraData={extraData} ref={forwardRef} keyExtractor={keyExtractor} data={messages} renderItem={renderItem} inverted={inverted} automaticallyAdjustContentInsets={false} style={stylesCommon.fill} {...invertibleScrollViewProps} ListEmptyComponent={renderChatEmpty} ListFooterComponent={inverted ? ListHeaderComponent : ListFooterComponent} ListHeaderComponent={inverted ? ListFooterComponent : ListHeaderComponent} onScroll={scrollHandler} scrollEventThrottle={1} onEndReached={onEndReached} onEndReachedThreshold={0.1} {...listViewProps} onLayout={onLayoutList} CellRendererComponent={renderCell}/>
{isScrollToBottomEnabled
? renderScrollToBottomWrapper()
: null}
<DayAnimated scrolledY={scrolledY} daysPositions={daysPositions} listHeight={listHeight} messages={messages} isLoadingEarlier={isLoadingEarlier}/>
</View>);
}
export default MessageContainer;
//# sourceMappingURL=index.js.map