import React, { Component } from 'react'; import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native'; import { connect } from 'react-redux'; import { IReduxState } from '../../../app/types'; import { translate } from '../../../base/i18n/functions.native'; import { groupMessagesBySender } from '../../../base/util/messageGrouping'; import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants'; import { getActiveChatSearchMatch } from '../../functions'; import { IMessage } from '../../types'; import ChatMessage from './ChatMessage'; import styles from './styles'; /** * A single flat list row: one message plus its grouping flags. */ interface IMessageRow { message: IMessage; showAvatar: boolean; showDisplayName: boolean; showTimestamp: boolean; } interface IProps { _activeMatch?: IMessage; messages: IMessage[]; t: Function; } /** * Implements a container to render all the chat messages in a conference. */ class MessageContainer extends Component { static defaultProps = { messages: [] as IMessage[] }; _flatListRef: React.RefObject>; /** * Instantiates a new instance of the component. * * @inheritdoc */ constructor(props: IProps) { super(props); this._flatListRef = React.createRef(); this._keyExtractor = this._keyExtractor.bind(this); this._renderListEmptyComponent = this._renderListEmptyComponent.bind(this); this._renderMessage = this._renderMessage.bind(this); this._getMessageRows = this._getMessageRows.bind(this); this._onScrollToIndexFailed = this._onScrollToIndexFailed.bind(this); } /** * Implements {@code Component#componentDidUpdate}. * * @inheritdoc */ override componentDidUpdate(prevProps: IProps) { const { _activeMatch } = this.props; if (_activeMatch && _activeMatch.messageId !== prevProps._activeMatch?.messageId) { this._scrollToActiveMatch(); } } /** * Scrolls the list so the active search match message is visible. * * @returns {void} */ _scrollToActiveMatch() { const { _activeMatch } = this.props; if (!_activeMatch) { return; } const data = this._getMessageRows(); const index = data.findIndex(row => row.message.messageId === _activeMatch.messageId); if (index === -1) { return; } requestAnimationFrame(() => { this._flatListRef.current?.scrollToIndex({ animated: true, index, viewPosition: 0.5 }); }); } /** * Fallback for when scrollToIndex fires before the target row is measured. * * @param {Object} info - Info about the failed scroll attempt. * @returns {void} */ _onScrollToIndexFailed(info: { averageItemLength: number; index: number; }) { // Jump near the target so it renders, then retry. this._flatListRef.current?.scrollToOffset({ animated: false, offset: info.averageItemLength * info.index }); setTimeout(() => { this._flatListRef.current?.scrollToIndex({ animated: true, index: info.index, viewPosition: 0.5 }); }, 100); } /** * Implements {@code Component#render}. * * @inheritdoc */ override render() { const data = this._getMessageRows(); const noMessages = data.length === 0; return ( ); } /** * Key extractor for the flatlist. * * @param {IMessageRow} item - The row whose key we need to generate. * @returns {string} */ _keyExtractor(item: IMessageRow) { return item.message.messageId; } /** * Renders a message when there are no messages in the chat yet. * * @returns {React$Element} */ _renderListEmptyComponent() { const { t } = this.props; return ( { t('chat.noMessagesMessage') } ); } /** * Renders a single chat message. * * @param {Object} row - The row containing the message and its grouping flags. * @returns {React$Element<*>} */ _renderMessage({ item }: { item: IMessageRow; }) { return ( ); } /** * Builds one row per message with its grouping flags. * * @returns {IMessageRow[]} */ _getMessageRows(): IMessageRow[] { const rows: IMessageRow[] = []; for (const group of groupMessagesBySender(this.props.messages)) { const groupType = group.messages[0].messageType; const oldestIndex = group.messages.length - 1; group.messages.forEach((message, index) => { rows.push({ message, showAvatar: groupType !== MESSAGE_TYPE_LOCAL && index === oldestIndex, showDisplayName: groupType === MESSAGE_TYPE_REMOTE && index === oldestIndex, showTimestamp: index === 0 }); }); } return rows; } } function _mapStateToProps(state: IReduxState) { return { _activeMatch: getActiveChatSearchMatch(state) }; } export default translate(connect(_mapStateToProps)(MessageContainer));