react-native-gifted-chat
Version:
The most complete chat UI for React Native
241 lines • 10.2 kB
JavaScript
import React, { createRef, useEffect, useMemo, useRef, useState, useCallback, } from 'react';
import { View, useColorScheme, } from 'react-native';
import { ActionSheetProvider, } from '@expo/react-native-action-sheet';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { KeyboardAvoidingView, KeyboardProvider } from 'react-native-keyboard-controller';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { TEST_ID } from '../Constant';
import { GiftedChatContext } from '../GiftedChatContext';
import { InputToolbar } from '../InputToolbar';
import { MessagesContainer } from '../MessagesContainer';
import stylesCommon from '../styles';
import { renderComponentOrElement } from '../utils';
import styles from './styles';
dayjs.extend(localizedFormat);
function GiftedChat(props) {
const { messages = [], initialText = '', isTyping,
// "random" function from here: https://stackoverflow.com/a/8084248/3452513
// we do not use uuid since it would add extra native dependency (https://www.npmjs.com/package/react-native-get-random-values)
// lib's user can decide which algorithm to use and pass it as a prop
messageIdGenerator = () => (Math.random() + 1).toString(36).substring(7), user = {}, onSend, locale = 'en', colorScheme: colorSchemeProp, renderLoading, actionSheet, textInputProps, renderChatFooter, renderInputToolbar, isInverted = true,
// Reply props
reply, } = props;
// Extract reply props for internal use
const replyMessageProp = reply?.message;
const onClearReply = reply?.onClear;
const onSwipeToReply = reply?.swipe?.onSwipe;
const renderReplyPreview = reply?.renderPreview;
const replyPreviewContainerStyle = reply?.previewStyle?.containerStyle;
const replyPreviewTextStyle = reply?.previewStyle?.textStyle;
const systemColorScheme = useColorScheme();
const colorScheme = colorSchemeProp !== undefined ? colorSchemeProp : systemColorScheme;
const actionSheetRef = useRef(null);
const insets = useSafeAreaInsets();
const messagesContainerRef = useMemo(() => props.messagesContainerRef || createRef(), [props.messagesContainerRef]);
const textInputRef = useMemo(() => props.textInputRef || createRef(), [props.textInputRef]);
const [isInitialized, setIsInitialized] = useState(false);
const [text, setText] = useState(() => props.text || '');
const [internalReplyMessage, setInternalReplyMessage] = useState(null);
// Use controlled or uncontrolled reply state
const replyMessage = replyMessageProp !== undefined ? replyMessageProp : internalReplyMessage;
const getTextFromProp = useCallback((fallback) => {
if (props.text === undefined)
return fallback;
return props.text;
}, [props.text]);
const scrollToBottom = useCallback((isAnimated = true) => {
if (!messagesContainerRef?.current)
return;
if (isInverted) {
messagesContainerRef.current.scrollToOffset({
offset: 0,
animated: isAnimated,
});
return;
}
messagesContainerRef.current.scrollToEnd({ animated: isAnimated });
}, [isInverted, messagesContainerRef]);
const handleSwipeToReply = useCallback((message) => {
if (replyMessageProp === undefined)
// Uncontrolled mode: manage state internally
setInternalReplyMessage({
_id: message._id,
text: message.text,
user: message.user,
image: message.image,
audio: message.audio,
});
onSwipeToReply?.(message);
}, [replyMessageProp, onSwipeToReply]);
const clearReply = useCallback(() => {
if (replyMessageProp === undefined)
// Uncontrolled mode: manage state internally
setInternalReplyMessage(null);
onClearReply?.();
}, [replyMessageProp, onClearReply]);
const renderMessages = useMemo(() => {
if (!isInitialized)
return null;
const { messagesContainerStyle, ...messagesContainerProps } = props;
return (<View style={[stylesCommon.fill, messagesContainerStyle]}>
<MessagesContainer {...messagesContainerProps} isInverted={isInverted} messages={messages} forwardRef={messagesContainerRef} isTyping={isTyping} reply={{
...reply,
swipe: reply?.swipe ? {
...reply.swipe,
onSwipe: handleSwipeToReply,
} : undefined,
}}/>
{renderComponentOrElement(renderChatFooter, {})}
</View>);
}, [
isInitialized,
isTyping,
messages,
props,
isInverted,
messagesContainerRef,
renderChatFooter,
reply,
handleSwipeToReply,
]);
const notifyInputTextReset = useCallback(() => {
props.textInputProps?.onChangeText?.('');
}, [props.textInputProps]);
const resetInputToolbar = useCallback(() => {
textInputRef.current?.clear();
notifyInputTextReset();
setText(getTextFromProp(''));
}, [
getTextFromProp,
textInputRef,
notifyInputTextReset,
]);
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?.(),
// Attach reply message if exists
...(replyMessage ? { replyMessage } : {}),
};
});
if (shouldResetInputToolbar === true)
resetInputToolbar();
// Clear reply after sending
clearReply();
onSend?.(newMessages);
setTimeout(() => scrollToBottom(), 10);
}, [messageIdGenerator, onSend, user, resetInputToolbar, scrollToBottom, replyMessage, clearReply]);
const _onChangeText = useCallback((text) => {
props.textInputProps?.onChangeText?.(text);
// Only set state if it's not being overridden by a prop.
if (props.text === undefined)
setText(text);
}, [props.text, props.textInputProps]);
const onInitialLayoutViewLayout = useCallback((e) => {
if (isInitialized)
return;
const { layout } = e.nativeEvent;
if (layout.height <= 0)
return;
notifyInputTextReset();
setIsInitialized(true);
setText(getTextFromProp(initialText));
}, [isInitialized, initialText, notifyInputTextReset, getTextFromProp]);
const inputToolbarFragment = useMemo(() => {
if (!isInitialized)
return null;
const inputToolbarProps = {
...props,
text: getTextFromProp(text),
onSend: _onSend,
textInputProps: {
...textInputProps,
onChangeText: _onChangeText,
ref: textInputRef,
},
// Reply preview props
replyMessage,
onClearReply: clearReply,
renderReplyPreview,
replyPreviewContainerStyle,
replyPreviewTextStyle,
};
if (renderInputToolbar)
return renderComponentOrElement(renderInputToolbar, inputToolbarProps);
return <InputToolbar {...inputToolbarProps}/>;
}, [
isInitialized,
_onSend,
getTextFromProp,
props,
text,
renderInputToolbar,
textInputRef,
textInputProps,
_onChangeText,
replyMessage,
clearReply,
renderReplyPreview,
replyPreviewContainerStyle,
replyPreviewTextStyle,
]);
const contextValues = useMemo(() => ({
actionSheet: actionSheet ||
(() => ({
showActionSheetWithOptions: actionSheetRef.current.showActionSheetWithOptions,
})),
getLocale: () => locale,
getColorScheme: () => colorScheme,
}), [actionSheet, locale, colorScheme]);
useEffect(() => {
if (props.text != null)
setText(props.text);
}, [props.text]);
return (<GiftedChatContext.Provider value={contextValues}>
<ActionSheetProvider ref={actionSheetRef}>
<View testID={TEST_ID.WRAPPER} style={[stylesCommon.fill, styles.contentContainer]} onLayout={onInitialLayoutViewLayout}>
{/* @ts-expect-error */}
<KeyboardAvoidingView behavior='translate-with-padding' keyboardVerticalOffset={insets.top} style={stylesCommon.fill} {...props.keyboardAvoidingViewProps}>
<View style={[stylesCommon.fill, !isInitialized && styles.hidden]}>
{renderMessages}
{inputToolbarFragment}
</View>
{!isInitialized && renderComponentOrElement(renderLoading, {})}
</KeyboardAvoidingView>
</View>
</ActionSheetProvider>
</GiftedChatContext.Provider>);
}
function GiftedChatWrapper(props) {
const { keyboardProviderProps, ...rest } = props;
return (<GestureHandlerRootView style={styles.fill}>
<SafeAreaProvider>
<KeyboardProvider statusBarTranslucent navigationBarTranslucent {...keyboardProviderProps}>
<GiftedChat {...rest}/>
</KeyboardProvider>
</SafeAreaProvider>
</GestureHandlerRootView>);
}
GiftedChatWrapper.append = (currentMessages = [], messages, isInverted = true) => {
if (!Array.isArray(messages))
messages = [messages];
return isInverted
? messages.concat(currentMessages)
: currentMessages.concat(messages);
};
GiftedChatWrapper.prepend = (currentMessages = [], messages, isInverted = true) => {
if (!Array.isArray(messages))
messages = [messages];
return isInverted
? currentMessages.concat(messages)
: messages.concat(currentMessages);
};
export { GiftedChatWrapper as GiftedChat };
//# sourceMappingURL=index.js.map