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
JavaScript
/* 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,
};