UNPKG

20.3 kBJavaScriptView Raw
1import PropTypes from 'prop-types';
2import React from 'react';
3import { Platform, StyleSheet, View, SafeAreaView, KeyboardAvoidingView, } from 'react-native';
4import { ActionSheetProvider, } from '@expo/react-native-action-sheet';
5import uuid from 'uuid';
6import { getBottomSpace } from 'react-native-iphone-x-helper';
7import dayjs from 'dayjs';
8import localizedFormat from 'dayjs/plugin/localizedFormat';
9import * as utils from './utils';
10import Actions from './Actions';
11import Avatar from './Avatar';
12import Bubble from './Bubble';
13import SystemMessage from './SystemMessage';
14import MessageImage from './MessageImage';
15import MessageText from './MessageText';
16import Composer from './Composer';
17import Day from './Day';
18import InputToolbar from './InputToolbar';
19import LoadEarlier from './LoadEarlier';
20import Message from './Message';
21import MessageContainer from './MessageContainer';
22import Send from './Send';
23import Time from './Time';
24import GiftedAvatar from './GiftedAvatar';
25import { MIN_COMPOSER_HEIGHT, MAX_COMPOSER_HEIGHT, DEFAULT_PLACEHOLDER, TIME_FORMAT, DATE_FORMAT, } from './Constant';
26dayjs.extend(localizedFormat);
27class GiftedChat extends React.Component {
28 constructor(props) {
29 super(props);
30 this._isMounted = false;
31 this._keyboardHeight = 0;
32 this._bottomOffset = 0;
33 this._maxHeight = undefined;
34 this._isFirstLayout = true;
35 this._locale = 'en';
36 this.invertibleScrollViewProps = undefined;
37 this._actionSheetRef = undefined;
38 this._messageContainerRef = React.createRef();
39 this.state = {
40 isInitialized: false,
41 composerHeight: this.props.minComposerHeight,
42 messagesContainerHeight: undefined,
43 typingDisabled: false,
44 text: undefined,
45 messages: undefined,
46 };
47 this.getLocale = () => this._locale;
48 this.safeAreaSupport = (bottomOffset) => {
49 return bottomOffset === this._bottomOffset
50 ? this.getBottomOffset()
51 ? this.getBottomOffset()
52 : getBottomSpace()
53 : bottomOffset;
54 };
55 this.onKeyboardWillShow = (e) => {
56 if (this.props.isKeyboardInternallyHandled) {
57 this.setIsTypingDisabled(true);
58 this.setKeyboardHeight(e.endCoordinates ? e.endCoordinates.height : e.end.height);
59 this.setBottomOffset(this.safeAreaSupport(this.props.bottomOffset));
60 const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard();
61 this.setState({
62 messagesContainerHeight: newMessagesContainerHeight,
63 });
64 }
65 };
66 this.onKeyboardWillHide = (_e) => {
67 if (this.props.isKeyboardInternallyHandled) {
68 this.setIsTypingDisabled(true);
69 this.setKeyboardHeight(0);
70 this.setBottomOffset(0);
71 const newMessagesContainerHeight = this.getBasicMessagesContainerHeight();
72 this.setState({
73 messagesContainerHeight: newMessagesContainerHeight,
74 });
75 }
76 };
77 this.onKeyboardDidShow = (e) => {
78 if (Platform.OS === 'android') {
79 this.onKeyboardWillShow(e);
80 }
81 this.setIsTypingDisabled(false);
82 };
83 this.onKeyboardDidHide = (e) => {
84 if (Platform.OS === 'android') {
85 this.onKeyboardWillHide(e);
86 }
87 this.setIsTypingDisabled(false);
88 };
89 this.onSend = (messages = [], shouldResetInputToolbar = false) => {
90 if (!Array.isArray(messages)) {
91 messages = [messages];
92 }
93 const newMessages = messages.map(message => {
94 return {
95 ...message,
96 user: this.props.user,
97 createdAt: new Date(),
98 _id: this.props.messageIdGenerator && this.props.messageIdGenerator(),
99 };
100 });
101 if (shouldResetInputToolbar === true) {
102 this.setIsTypingDisabled(true);
103 this.resetInputToolbar();
104 }
105 if (this.props.onSend) {
106 this.props.onSend(newMessages);
107 }
108 if (shouldResetInputToolbar === true) {
109 setTimeout(() => {
110 if (this.getIsMounted() === true) {
111 this.setIsTypingDisabled(false);
112 }
113 }, 100);
114 }
115 };
116 this.onInputSizeChanged = (size) => {
117 const newComposerHeight = Math.max(this.props.minComposerHeight, Math.min(this.props.maxComposerHeight, size.height));
118 const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight);
119 this.setState({
120 composerHeight: newComposerHeight,
121 messagesContainerHeight: newMessagesContainerHeight,
122 });
123 };
124 this.onInputTextChanged = (text) => {
125 if (this.getIsTypingDisabled()) {
126 return;
127 }
128 if (this.props.onInputTextChanged) {
129 this.props.onInputTextChanged(text);
130 }
131 // Only set state if it's not being overridden by a prop.
132 if (this.props.text === undefined) {
133 this.setState({ text });
134 }
135 };
136 this.onInitialLayoutViewLayout = (e) => {
137 const { layout } = e.nativeEvent;
138 if (layout.height <= 0) {
139 return;
140 }
141 this.notifyInputTextReset();
142 this.setMaxHeight(layout.height);
143 const newComposerHeight = this.props.minComposerHeight;
144 const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight);
145 const initialText = this.props.initialText || '';
146 this.setState({
147 isInitialized: true,
148 text: this.getTextFromProp(initialText),
149 composerHeight: newComposerHeight,
150 messagesContainerHeight: newMessagesContainerHeight,
151 });
152 };
153 this.onMainViewLayout = (e) => {
154 // fix an issue when keyboard is dismissing during the initialization
155 const { layout } = e.nativeEvent;
156 if (this.getMaxHeight() !== layout.height ||
157 this.getIsFirstLayout() === true) {
158 this.setMaxHeight(layout.height);
159 this.setState({
160 messagesContainerHeight: this._keyboardHeight > 0
161 ? this.getMessagesContainerHeightWithKeyboard()
162 : this.getBasicMessagesContainerHeight(),
163 });
164 }
165 if (this.getIsFirstLayout() === true) {
166 this.setIsFirstLayout(false);
167 }
168 };
169 this.invertibleScrollViewProps = {
170 inverted: this.props.inverted,
171 keyboardShouldPersistTaps: this.props.keyboardShouldPersistTaps,
172 onKeyboardWillShow: this.onKeyboardWillShow,
173 onKeyboardWillHide: this.onKeyboardWillHide,
174 onKeyboardDidShow: this.onKeyboardDidShow,
175 onKeyboardDidHide: this.onKeyboardDidHide,
176 };
177 }
178 static append(currentMessages = [], messages, inverted = true) {
179 if (!Array.isArray(messages)) {
180 messages = [messages];
181 }
182 return inverted
183 ? messages.concat(currentMessages)
184 : currentMessages.concat(messages);
185 }
186 static prepend(currentMessages = [], messages, inverted = true) {
187 if (!Array.isArray(messages)) {
188 messages = [messages];
189 }
190 return inverted
191 ? currentMessages.concat(messages)
192 : messages.concat(currentMessages);
193 }
194 getChildContext() {
195 return {
196 actionSheet: this.props.actionSheet || (() => this._actionSheetRef.getContext()),
197 getLocale: this.getLocale,
198 };
199 }
200 componentDidMount() {
201 const { messages, text } = this.props;
202 this.setIsMounted(true);
203 this.initLocale();
204 this.setMessages(messages || []);
205 this.setTextFromProp(text);
206 }
207 componentWillUnmount() {
208 this.setIsMounted(false);
209 }
210 componentDidUpdate(prevProps = {}) {
211 const { messages, text, inverted } = this.props;
212 if (this.props !== prevProps) {
213 this.setMessages(messages || []);
214 }
215 if (inverted === false &&
216 messages &&
217 prevProps.messages &&
218 messages.length !== prevProps.messages.length) {
219 setTimeout(() => this.scrollToBottom(false), 200);
220 }
221 if (text !== prevProps.text) {
222 this.setTextFromProp(text);
223 }
224 }
225 initLocale() {
226 if (this.props.locale === null) {
227 this.setLocale('en');
228 }
229 else {
230 this.setLocale(this.props.locale || 'en');
231 }
232 }
233 setLocale(locale) {
234 this._locale = locale;
235 }
236 setTextFromProp(textProp) {
237 // Text prop takes precedence over state.
238 if (textProp !== undefined && textProp !== this.state.text) {
239 this.setState({ text: textProp });
240 }
241 }
242 getTextFromProp(fallback) {
243 if (this.props.text === undefined) {
244 return fallback;
245 }
246 return this.props.text;
247 }
248 setMessages(messages) {
249 this.setState({ messages });
250 }
251 getMessages() {
252 return this.state.messages;
253 }
254 setMaxHeight(height) {
255 this._maxHeight = height;
256 }
257 getMaxHeight() {
258 return this._maxHeight;
259 }
260 setKeyboardHeight(height) {
261 this._keyboardHeight = height;
262 }
263 getKeyboardHeight() {
264 if (Platform.OS === 'android' && !this.props.forceGetKeyboardHeight) {
265 // For android: on-screen keyboard resized main container and has own height.
266 // @see https://developer.android.com/training/keyboard-input/visibility.html
267 // So for calculate the messages container height ignore keyboard height.
268 return 0;
269 }
270 return this._keyboardHeight;
271 }
272 setBottomOffset(value) {
273 this._bottomOffset = value;
274 }
275 getBottomOffset() {
276 return this._bottomOffset;
277 }
278 setIsFirstLayout(value) {
279 this._isFirstLayout = value;
280 }
281 getIsFirstLayout() {
282 return this._isFirstLayout;
283 }
284 setIsTypingDisabled(value) {
285 this.setState({
286 typingDisabled: value,
287 });
288 }
289 getIsTypingDisabled() {
290 return this.state.typingDisabled;
291 }
292 setIsMounted(value) {
293 this._isMounted = value;
294 }
295 getIsMounted() {
296 return this._isMounted;
297 }
298 getMinInputToolbarHeight() {
299 return this.props.renderAccessory
300 ? this.props.minInputToolbarHeight * 2
301 : this.props.minInputToolbarHeight;
302 }
303 calculateInputToolbarHeight(composerHeight) {
304 return (composerHeight +
305 (this.getMinInputToolbarHeight() - this.props.minComposerHeight));
306 }
307 /**
308 * Returns the height, based on current window size, without taking the keyboard into account.
309 */
310 getBasicMessagesContainerHeight(composerHeight = this.state.composerHeight) {
311 return (this.getMaxHeight() - this.calculateInputToolbarHeight(composerHeight));
312 }
313 /**
314 * Returns the height, based on current window size, taking the keyboard into account.
315 */
316 getMessagesContainerHeightWithKeyboard(composerHeight = this.state.composerHeight) {
317 return (this.getBasicMessagesContainerHeight(composerHeight) -
318 this.getKeyboardHeight() +
319 this.getBottomOffset());
320 }
321 scrollToBottom(animated = true) {
322 if (this._messageContainerRef && this._messageContainerRef.current) {
323 const { inverted } = this.props;
324 if (!inverted) {
325 this._messageContainerRef.current.scrollToEnd({ animated });
326 }
327 else {
328 this._messageContainerRef.current.scrollToOffset({
329 offset: 0,
330 animated,
331 });
332 }
333 }
334 }
335 renderMessages() {
336 const { messagesContainerStyle, ...messagesContainerProps } = this.props;
337 const fragment = (<View style={[
338 {
339 height: this.state.messagesContainerHeight,
340 },
341 messagesContainerStyle,
342 ]}>
343 <MessageContainer {...messagesContainerProps} invertibleScrollViewProps={this.invertibleScrollViewProps} messages={this.getMessages()} forwardRef={this._messageContainerRef} isTyping={this.props.isTyping}/>
344 {this.renderChatFooter()}
345 </View>);
346 return this.props.isKeyboardInternallyHandled ? (<KeyboardAvoidingView enabled>{fragment}</KeyboardAvoidingView>) : (fragment);
347 }
348 resetInputToolbar() {
349 if (this.textInput) {
350 this.textInput.clear();
351 }
352 this.notifyInputTextReset();
353 const newComposerHeight = this.props.minComposerHeight;
354 const newMessagesContainerHeight = this.getMessagesContainerHeightWithKeyboard(newComposerHeight);
355 this.setState({
356 text: this.getTextFromProp(''),
357 composerHeight: newComposerHeight,
358 messagesContainerHeight: newMessagesContainerHeight,
359 });
360 }
361 focusTextInput() {
362 if (this.textInput) {
363 this.textInput.focus();
364 }
365 }
366 notifyInputTextReset() {
367 if (this.props.onInputTextChanged) {
368 this.props.onInputTextChanged('');
369 }
370 }
371 renderInputToolbar() {
372 const inputToolbarProps = {
373 ...this.props,
374 text: this.getTextFromProp(this.state.text),
375 composerHeight: Math.max(this.props.minComposerHeight, this.state.composerHeight),
376 onSend: this.onSend,
377 onInputSizeChanged: this.onInputSizeChanged,
378 onTextChanged: this.onInputTextChanged,
379 textInputProps: {
380 ...this.props.textInputProps,
381 ref: (textInput) => (this.textInput = textInput),
382 maxLength: this.getIsTypingDisabled() ? 0 : this.props.maxInputLength,
383 },
384 };
385 if (this.props.renderInputToolbar) {
386 return this.props.renderInputToolbar(inputToolbarProps);
387 }
388 return <InputToolbar {...inputToolbarProps}/>;
389 }
390 renderChatFooter() {
391 if (this.props.renderChatFooter) {
392 return this.props.renderChatFooter();
393 }
394 return null;
395 }
396 renderLoading() {
397 if (this.props.renderLoading) {
398 return this.props.renderLoading();
399 }
400 return null;
401 }
402 render() {
403 if (this.state.isInitialized === true) {
404 const { wrapInSafeArea } = this.props;
405 const Wrapper = wrapInSafeArea ? SafeAreaView : View;
406 return (<Wrapper style={styles.safeArea}>
407 <ActionSheetProvider ref={(component) => (this._actionSheetRef = component)}>
408 <View style={styles.container} onLayout={this.onMainViewLayout}>
409 {this.renderMessages()}
410 {this.renderInputToolbar()}
411 </View>
412 </ActionSheetProvider>
413 </Wrapper>);
414 }
415 return (<View style={styles.container} onLayout={this.onInitialLayoutViewLayout}>
416 {this.renderLoading()}
417 </View>);
418 }
419}
420GiftedChat.childContextTypes = {
421 actionSheet: PropTypes.func,
422 getLocale: PropTypes.func,
423};
424GiftedChat.defaultProps = {
425 messages: [],
426 messagesContainerStyle: undefined,
427 text: undefined,
428 placeholder: DEFAULT_PLACEHOLDER,
429 disableComposer: false,
430 messageIdGenerator: () => uuid.v4(),
431 user: {},
432 onSend: () => { },
433 locale: null,
434 timeFormat: TIME_FORMAT,
435 dateFormat: DATE_FORMAT,
436 loadEarlier: false,
437 onLoadEarlier: () => { },
438 isLoadingEarlier: false,
439 renderLoading: null,
440 renderLoadEarlier: null,
441 renderAvatar: undefined,
442 showUserAvatar: false,
443 actionSheet: null,
444 onPressAvatar: null,
445 onLongPressAvatar: null,
446 renderUsernameOnMessage: false,
447 renderAvatarOnTop: false,
448 renderBubble: null,
449 renderSystemMessage: null,
450 onLongPress: null,
451 renderMessage: null,
452 renderMessageText: null,
453 renderMessageImage: null,
454 imageProps: {},
455 videoProps: {},
456 audioProps: {},
457 lightboxProps: {},
458 textInputProps: {},
459 listViewProps: {},
460 renderCustomView: null,
461 isCustomViewBottom: false,
462 renderDay: null,
463 renderTime: null,
464 renderFooter: null,
465 renderChatEmpty: null,
466 renderChatFooter: null,
467 renderInputToolbar: null,
468 renderComposer: null,
469 renderActions: null,
470 renderSend: null,
471 renderAccessory: null,
472 isKeyboardInternallyHandled: true,
473 onPressActionButton: null,
474 bottomOffset: 0,
475 minInputToolbarHeight: 44,
476 keyboardShouldPersistTaps: Platform.select({
477 ios: 'never',
478 android: 'always',
479 default: 'never',
480 }),
481 onInputTextChanged: null,
482 maxInputLength: null,
483 forceGetKeyboardHeight: false,
484 inverted: true,
485 extraData: null,
486 minComposerHeight: MIN_COMPOSER_HEIGHT,
487 maxComposerHeight: MAX_COMPOSER_HEIGHT,
488 wrapInSafeArea: true,
489};
490GiftedChat.propTypes = {
491 messages: PropTypes.arrayOf(PropTypes.object),
492 messagesContainerStyle: utils.StylePropType,
493 text: PropTypes.string,
494 initialText: PropTypes.string,
495 placeholder: PropTypes.string,
496 disableComposer: PropTypes.bool,
497 messageIdGenerator: PropTypes.func,
498 user: PropTypes.object,
499 onSend: PropTypes.func,
500 locale: PropTypes.string,
501 timeFormat: PropTypes.string,
502 dateFormat: PropTypes.string,
503 isKeyboardInternallyHandled: PropTypes.bool,
504 loadEarlier: PropTypes.bool,
505 onLoadEarlier: PropTypes.func,
506 isLoadingEarlier: PropTypes.bool,
507 renderLoading: PropTypes.func,
508 renderLoadEarlier: PropTypes.func,
509 renderAvatar: PropTypes.func,
510 showUserAvatar: PropTypes.bool,
511 actionSheet: PropTypes.func,
512 onPressAvatar: PropTypes.func,
513 onLongPressAvatar: PropTypes.func,
514 renderUsernameOnMessage: PropTypes.bool,
515 renderAvatarOnTop: PropTypes.bool,
516 isCustomViewBottom: PropTypes.bool,
517 renderBubble: PropTypes.func,
518 renderSystemMessage: PropTypes.func,
519 onLongPress: PropTypes.func,
520 renderMessage: PropTypes.func,
521 renderMessageText: PropTypes.func,
522 renderMessageImage: PropTypes.func,
523 imageProps: PropTypes.object,
524 videoProps: PropTypes.object,
525 audioProps: PropTypes.object,
526 lightboxProps: PropTypes.object,
527 renderCustomView: PropTypes.func,
528 renderDay: PropTypes.func,
529 renderTime: PropTypes.func,
530 renderFooter: PropTypes.func,
531 renderChatEmpty: PropTypes.func,
532 renderChatFooter: PropTypes.func,
533 renderInputToolbar: PropTypes.func,
534 renderComposer: PropTypes.func,
535 renderActions: PropTypes.func,
536 renderSend: PropTypes.func,
537 renderAccessory: PropTypes.func,
538 onPressActionButton: PropTypes.func,
539 bottomOffset: PropTypes.number,
540 minInputToolbarHeight: PropTypes.number,
541 listViewProps: PropTypes.object,
542 keyboardShouldPersistTaps: PropTypes.oneOf(['always', 'never', 'handled']),
543 onInputTextChanged: PropTypes.func,
544 maxInputLength: PropTypes.number,
545 forceGetKeyboardHeight: PropTypes.bool,
546 inverted: PropTypes.bool,
547 textInputProps: PropTypes.object,
548 extraData: PropTypes.object,
549 minComposerHeight: PropTypes.number,
550 maxComposerHeight: PropTypes.number,
551 alignTop: PropTypes.bool,
552 wrapInSafeArea: PropTypes.bool,
553};
554const styles = StyleSheet.create({
555 container: {
556 flex: 1,
557 },
558 safeArea: {
559 flex: 1,
560 },
561});
562export { GiftedChat, Actions, Avatar, Bubble, SystemMessage, MessageImage, MessageText, Composer, Day, InputToolbar, LoadEarlier, Message, MessageContainer, Send, Time, GiftedAvatar, utils, };
563//# sourceMappingURL=GiftedChat.js.map
\No newline at end of file