UNPKG

react-native-gifted-chat-video-support

Version:

The most complete chat UI for React Native , now support send video thanks to react-native-video-player

633 lines (563 loc) 16.9 kB
/* eslint no-param-reassign: 0, no-use-before-define: ["error", { "variables": false }], no-return-assign: 0, no-mixed-operators: 0, react/sort-comp: 0 */ import PropTypes from 'prop-types'; import React from 'react'; import { Animated, Platform, StyleSheet, View } from 'react-native'; import ActionSheet from '@expo/react-native-action-sheet'; import moment from 'moment'; import uuid from 'uuid'; import * as utils from './utils'; import Actions from './Actions'; import Avatar from './Avatar'; import Bubble from './Bubble'; import SystemMessage from './SystemMessage'; import MessageImage from './MessageImage'; import MessageText from './MessageText'; import Composer from './Composer'; import Day from './Day'; import InputToolbar from './InputToolbar'; import LoadEarlier from './LoadEarlier'; import Message from './Message'; import MessageContainer from './MessageContainer'; import Send from './Send'; import Time from './Time'; import GiftedAvatar from './GiftedAvatar'; import { MIN_COMPOSER_HEIGHT, MAX_COMPOSER_HEIGHT, DEFAULT_PLACEHOLDER, TIME_FORMAT, DATE_FORMAT, } from './Constant'; class GiftedChat extends React.Component { constructor(props) { super(props); // default values this._isMounted = false; this._keyboardHeight = 0; this._bottomOffset = 0; this._maxHeight = null; this._isFirstLayout = true; this._locale = 'en'; this._messages = []; this.state = { isInitialized: false, // initialization will calculate maxHeight before rendering the chat composerHeight: MIN_COMPOSER_HEIGHT, messagesContainerHeight: null, typingDisabled: false, }; this.onKeyboardWillShow = this.onKeyboardWillShow.bind(this); this.onKeyboardWillHide = this.onKeyboardWillHide.bind(this); this.onKeyboardDidShow = this.onKeyboardDidShow.bind(this); this.onKeyboardDidHide = this.onKeyboardDidHide.bind(this); this.onSend = this.onSend.bind(this); this.getLocale = this.getLocale.bind(this); this.onInputSizeChanged = this.onInputSizeChanged.bind(this); this.onInputTextChanged = this.onInputTextChanged.bind(this); this.onMainViewLayout = this.onMainViewLayout.bind(this); this.onInitialLayoutViewLayout = this.onInitialLayoutViewLayout.bind(this); this.invertibleScrollViewProps = { inverted: this.props.inverted, keyboardShouldPersistTaps: this.props.keyboardShouldPersistTaps, onKeyboardWillShow: this.onKeyboardWillShow, onKeyboardWillHide: this.onKeyboardWillHide, onKeyboardDidShow: this.onKeyboardDidShow, onKeyboardDidHide: this.onKeyboardDidHide, }; } static append(currentMessages = [], messages, inverted = true) { if (!Array.isArray(messages)) { messages = [messages]; } return inverted ? messages.concat(currentMessages) : currentMessages.concat(messages); } static prepend(currentMessages = [], messages, inverted = true) { if (!Array.isArray(messages)) { messages = [messages]; } return inverted ? currentMessages.concat(messages) : messages.concat(currentMessages); } getChildContext() { return { actionSheet: () => this._actionSheetRef, getLocale: this.getLocale, }; } componentWillMount() { const { messages, text } = this.props; this.setIsMounted(true); this.initLocale(); this.setMessages(messages || []); this.setTextFromProp(text); } componentWillUnmount() { this.setIsMounted(false); } componentWillReceiveProps(nextProps = {}) { const { messages, text } = nextProps; this.setMessages(messages || []); this.setTextFromProp(text); } initLocale() { if (this.props.locale === null || moment.locales().indexOf(this.props.locale) === -1) { this.setLocale('en'); } else { this.setLocale(this.props.locale); } } setLocale(locale) { this._locale = locale; } getLocale() { return this._locale; } setTextFromProp(textProp) { // Text prop takes precedence over state. if (textProp !== undefined && textProp !== this.state.text) { this.setState({ text: textProp }); } } getTextFromProp(fallback) { if (this.props.text === undefined) { return fallback; } return this.props.text; } setMessages(messages) { this._messages = messages; } getMessages() { return this._messages; } setMaxHeight(height) { this._maxHeight = height; } getMaxHeight() { return this._maxHeight; } setKeyboardHeight(height) { this._keyboardHeight = height; } getKeyboardHeight() { if (Platform.OS === 'android' && !this.props.forceGetKeyboardHeight) { // For android: on-screen keyboard resized main container and has own height. // @see https://developer.android.com/training/keyboard-input/visibility.html // So for calculate the messages container height ignore keyboard height. return 0; } return this._keyboardHeight; } setBottomOffset(value) { this._bottomOffset = value; } getBottomOffset() { return this._bottomOffset; } setIsFirstLayout(value) { this._isFirstLayout = value; } getIsFirstLayout() { return this._isFirstLayout; } setIsTypingDisabled(value) { this.setState({ typingDisabled: value, }); } getIsTypingDisabled() { return this.state.typingDisabled; } setIsMounted(value) { this._isMounted = value; } getIsMounted() { return this._isMounted; } // TODO: setMinInputToolbarHeight getMinInputToolbarHeight() { return this.props.renderAccessory ? this.props.minInputToolbarHeight * 2 : this.props.minInputToolbarHeight; } calculateInputToolbarHeight(composerHeight) { return composerHeight + (this.getMinInputToolbarHeight() - MIN_COMPOSER_HEIGHT); } /** * Returns the height, based on current window size, without taking the keyboard into account. */ getBasicMessagesContainerHeight(composerHeight = this.state.composerHeight) { return this.getMaxHeight() - this.calculateInputToolbarHeight(composerHeight); } /** * Returns the height, based on current window size, taking the keyboard into account. */ getMessagesContainerHeightWithKeyboard(composerHeight = this.state.composerHeight) { return this.getBasicMessagesContainerHeight(composerHeight) - this.getKeyboardHeight() + this.getBottomOffset(); } prepareMessagesContainerHeight(value) { if (this.props.isAnimated === true) { return new Animated.Value(value); } return value; } onKeyboardWillShow(e) { this.setIsTypingDisabled(true); this.setKeyboardHeight(e.endCoordinates ? e.endCoordinates.height : e.end.height); this.setBottomOffset(this.props.bottomOffset); const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(); if (this.props.isAnimated === true) { Animated.timing(this.state.messagesContainerHeight, { toValue: newMessagesContainerHeight, duration: 210, }).start(); } else { this.setState({ messagesContainerHeight: newMessagesContainerHeight, }); } } onKeyboardWillHide() { this.setIsTypingDisabled(true); this.setKeyboardHeight(0); this.setBottomOffset(0); const newMessagesContainerHeight = this.getBasicMessagesContainerHeight(); if (this.props.isAnimated === true) { Animated.timing(this.state.messagesContainerHeight, { toValue: newMessagesContainerHeight, duration: 210, }).start(); } else { this.setState({ messagesContainerHeight: newMessagesContainerHeight, }); } } onKeyboardDidShow(e) { if (Platform.OS === 'android') { this.onKeyboardWillShow(e); } this.setIsTypingDisabled(false); } onKeyboardDidHide(e) { if (Platform.OS === 'android') { this.onKeyboardWillHide(e); } this.setIsTypingDisabled(false); } scrollToBottom(animated = true) { if (this._messageContainerRef === null) { return; } this._messageContainerRef.scrollTo({ y: 0, animated }); } renderMessages() { const AnimatedView = this.props.isAnimated === true ? Animated.View : View; return ( <AnimatedView style={{ height: this.state.messagesContainerHeight, }} > <MessageContainer {...this.props} invertibleScrollViewProps={this.invertibleScrollViewProps} messages={this.getMessages()} ref={(component) => (this._messageContainerRef = component)} /> {this.renderChatFooter()} </AnimatedView> ); } onSend(messages = [], shouldResetInputToolbar = false) { if (!Array.isArray(messages)) { messages = [messages]; } messages = messages.map((message) => { return { ...message, user: this.props.user, createdAt: new Date(), _id: this.props.messageIdGenerator(), }; }); if (shouldResetInputToolbar === true) { this.setIsTypingDisabled(true); this.resetInputToolbar(); } this.props.onSend(messages); this.scrollToBottom(); if (shouldResetInputToolbar === true) { setTimeout(() => { if (this.getIsMounted() === true) { this.setIsTypingDisabled(false); } }, 100); } } resetInputToolbar() { if (this.textInput) { this.textInput.clear(); } this.notifyInputTextReset(); const newComposerHeight = MIN_COMPOSER_HEIGHT; const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight); this.setState({ text: this.getTextFromProp(''), composerHeight: newComposerHeight, messagesContainerHeight: this.prepareMessagesContainerHeight(newMessagesContainerHeight), }); } focusTextInput() { if (this.textInput) { this.textInput.focus(); } } onInputSizeChanged(size) { const newComposerHeight = Math.max(MIN_COMPOSER_HEIGHT, Math.min(MAX_COMPOSER_HEIGHT, size.height)); const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight); this.setState({ composerHeight: newComposerHeight, messagesContainerHeight: this.prepareMessagesContainerHeight(newMessagesContainerHeight), }); } onInputTextChanged(text) { if (this.getIsTypingDisabled()) { return; } if (this.props.onInputTextChanged) { this.props.onInputTextChanged(text); } // Only set state if it's not being overridden by a prop. if (this.props.text === undefined) { this.setState({ text }); } } notifyInputTextReset() { if (this.props.onInputTextChanged) { this.props.onInputTextChanged(''); } } onInitialLayoutViewLayout(e) { const { layout } = e.nativeEvent; if (layout.height <= 0) { return; } this.notifyInputTextReset(); this.setMaxHeight(layout.height); const newComposerHeight = MIN_COMPOSER_HEIGHT; const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight); this.setState({ isInitialized: true, text: this.getTextFromProp(''), composerHeight: newComposerHeight, messagesContainerHeight: this.prepareMessagesContainerHeight(newMessagesContainerHeight), }); } onMainViewLayout(e) { // fix an issue when keyboard is dismissing during the initialization const { layout } = e.nativeEvent; if (this.getMaxHeight() !== layout.height || this.getIsFirstLayout() === true) { this.setMaxHeight(layout.height); this.setState({ messagesContainerHeight: this.prepareMessagesContainerHeight(this.getBasicMessagesContainerHeight()), }); } if (this.getIsFirstLayout() === true) { this.setIsFirstLayout(false); } } renderInputToolbar() { const inputToolbarProps = { ...this.props, text: this.getTextFromProp(this.state.text), composerHeight: Math.max(MIN_COMPOSER_HEIGHT, this.state.composerHeight), onSend: this.onSend, onInputSizeChanged: this.onInputSizeChanged, onTextChanged: this.onInputTextChanged, textInputProps: { ...this.props.textInputProps, ref: (textInput) => (this.textInput = textInput), maxLength: this.getIsTypingDisabled() ? 0 : this.props.maxInputLength, }, }; if (this.props.renderInputToolbar) { return this.props.renderInputToolbar(inputToolbarProps); } return ( <InputToolbar {...inputToolbarProps} /> ); } renderChatFooter() { if (this.props.renderChatFooter) { const footerProps = { ...this.props, }; return this.props.renderChatFooter(footerProps); } return null; } renderLoading() { if (this.props.renderLoading) { return this.props.renderLoading(); } return null; } render() { if (this.state.isInitialized === true) { return ( <ActionSheet ref={(component) => (this._actionSheetRef = component)}> <View style={styles.container} onLayout={this.onMainViewLayout}> {this.renderMessages()} {this.renderInputToolbar()} </View> </ActionSheet> ); } return ( <View style={styles.container} onLayout={this.onInitialLayoutViewLayout}> {this.renderLoading()} </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, }, }); GiftedChat.childContextTypes = { actionSheet: PropTypes.func, getLocale: PropTypes.func, }; GiftedChat.defaultProps = { messages: [], text: undefined, placeholder: DEFAULT_PLACEHOLDER, messageIdGenerator: () => uuid.v4(), user: {}, onSend: () => { }, locale: null, timeFormat: TIME_FORMAT, dateFormat: DATE_FORMAT, isAnimated: Platform.select({ ios: true, android: false, }), loadEarlier: false, onLoadEarlier: () => { }, isLoadingEarlier: false, renderLoading: null, renderLoadEarlier: null, renderAvatar: undefined, showUserAvatar: false, onPressAvatar: null, renderAvatarOnTop: false, renderBubble: null, renderSystemMessage: null, onLongPress: null, renderMessage: null, renderMessageText: null, renderMessageImage: null, imageProps: {}, videoProps: {}, lightboxProps: {}, textInputProps: {}, listViewProps: {}, renderCustomView: null, renderDay: null, renderTime: null, renderFooter: null, renderChatFooter: null, renderInputToolbar: null, renderComposer: null, renderActions: null, renderSend: null, renderAccessory: null, onPressActionButton: null, bottomOffset: 0, minInputToolbarHeight: 44, keyboardShouldPersistTaps: Platform.select({ ios: 'never', android: 'always', }), onInputTextChanged: null, maxInputLength: null, forceGetKeyboardHeight: false, inverted: true, }; GiftedChat.propTypes = { messages: PropTypes.arrayOf(PropTypes.object), text: PropTypes.string, placeholder: PropTypes.string, messageIdGenerator: PropTypes.func, user: PropTypes.object, onSend: PropTypes.func, locale: PropTypes.string, timeFormat: PropTypes.string, dateFormat: PropTypes.string, isAnimated: PropTypes.bool, loadEarlier: PropTypes.bool, onLoadEarlier: PropTypes.func, isLoadingEarlier: PropTypes.bool, renderLoading: PropTypes.func, renderLoadEarlier: PropTypes.func, renderAvatar: PropTypes.func, showUserAvatar: PropTypes.bool, onPressAvatar: PropTypes.func, renderAvatarOnTop: PropTypes.bool, renderBubble: PropTypes.func, renderSystemMessage: PropTypes.func, onLongPress: PropTypes.func, renderMessage: PropTypes.func, renderMessageText: PropTypes.func, renderMessageImage: PropTypes.func, imageProps: PropTypes.object, videoProps: PropTypes.object, lightboxProps: PropTypes.object, renderCustomView: PropTypes.func, renderDay: PropTypes.func, renderTime: PropTypes.func, renderFooter: PropTypes.func, renderChatFooter: PropTypes.func, renderInputToolbar: PropTypes.func, renderComposer: PropTypes.func, renderActions: PropTypes.func, renderSend: PropTypes.func, renderAccessory: PropTypes.func, onPressActionButton: PropTypes.func, bottomOffset: PropTypes.number, minInputToolbarHeight: PropTypes.number, listViewProps: PropTypes.object, keyboardShouldPersistTaps: PropTypes.oneOf(['always', 'never', 'handled']), onInputTextChanged: PropTypes.func, maxInputLength: PropTypes.number, forceGetKeyboardHeight: PropTypes.bool, inverted: PropTypes.bool, textInputProps: PropTypes.object, }; export { GiftedChat, Actions, Avatar, Bubble, SystemMessage, MessageImage, MessageText, Composer, Day, InputToolbar, LoadEarlier, Message, MessageContainer, Send, Time, GiftedAvatar, utils, };