import React, { Component } from 'react'; import { Text, View, ViewStyle } from 'react-native'; import { connect } from 'react-redux'; import { IReduxState } from '../../../app/types'; import Avatar from '../../../base/avatar/components/Avatar'; import { translate } from '../../../base/i18n/functions'; import Linkify from '../../../base/react/components/native/Linkify'; import { isGifEnabled, isGifMessage } from '../../../gifs/functions.native'; import { CHAR_LIMIT, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants'; import { getActiveChatSearchMatch, getCanReplyToMessage, getChatSearchQuery, getDisplayNameSuffix, getFormattedTimestamp, getHighlightSegments, getMessageText, getPrivateNoticeMessage, replaceNonUnicodeEmojis } from '../../functions'; import { IChatMessageProps } from '../../types'; import GifMessage from './GifMessage'; import PrivateMessageButton from './PrivateMessageButton'; import styles from './styles'; /** * Renders a single chat message. */ class ChatMessage extends Component { /** * Implements {@code Component#render}. * * @inheritdoc */ override render() { const { gifEnabled, message, knocking, isActiveMatch } = this.props; const localMessage = message.messageType === MESSAGE_TYPE_LOCAL; const { privateMessage, lobbyChat } = message; // Style arrays that need to be updated in various scenarios, such as // error messages or others. const detailsWrapperStyle: ViewStyle[] = [ styles.detailsWrapper as ViewStyle ]; const messageBubbleStyle: ViewStyle[] = [ styles.messageBubble as ViewStyle ]; if (localMessage) { // This is a message sent by the local participant. // The wrapper needs to be aligned to the right. detailsWrapperStyle.push(styles.ownMessageDetailsWrapper as ViewStyle); // The bubble needs some additional styling messageBubbleStyle.push(styles.localMessageBubble); } else if (message.messageType === MESSAGE_TYPE_ERROR) { // This is a system message. // The bubble needs some additional styling messageBubbleStyle.push(styles.systemMessageBubble); } else { // This is a remote message sent by a remote participant. // The bubble needs some additional styling messageBubbleStyle.push(styles.remoteMessageBubble); } if (privateMessage) { messageBubbleStyle.push(styles.privateMessageBubble); } if (lobbyChat && !knocking) { messageBubbleStyle.push(styles.lobbyMessageBubble); } if (isActiveMatch) { messageBubbleStyle.push(styles.activeMatchMessageBubble); } const messageText = message.isDeleted ? this.props.t(localMessage ? 'chat.deletedMessageByMe' : 'chat.deletedMessage') : getMessageText(this.props.message); return ( { this._renderAvatar() } { this._renderDisplayName() } { gifEnabled && isGifMessage(messageText) ? : this._renderMessageTextComponent(messageText) } { this._renderPrivateNotice() } { !message.isDeleted && this._renderPrivateReplyButton() } { this._renderTimestamp() } ); } /** * Renders the avatar of the sender. * * @returns {React.ReactElement<*>} */ _renderAvatar() { const { message } = this.props; return ( { this.props.showAvatar && } ); } /** * Renders the display name of the sender if necessary. * * @returns {React.ReactElement<*> | null} */ _renderDisplayName() { const { message, showDisplayName } = this.props; if (!showDisplayName) { return null; } const { displayName } = message; return ( { `${displayName}${getDisplayNameSuffix(message)}` } ); } /** * Renders the message text based on number of characters. * * @param {string} messageText - The message text. * @returns {React.ReactElement<*>} */ _renderMessageTextComponent(messageText: string) { const { highlightQuery, isActiveMatch } = this.props; if (messageText.length >= CHAR_LIMIT) { if (highlightQuery && !isActiveMatch) { return ( { this._renderHighlightedSegments(messageText, highlightQuery, isActiveMatch) } ); } return ( { messageText } ); } const processedText = replaceNonUnicodeEmojis(messageText); return ( { highlightQuery && !isActiveMatch ? this._renderHighlightedSegments(processedText, highlightQuery, isActiveMatch) : processedText } ); } /** * Splits text into highlight/non-highlight segments and renders them. * * @param {string} text - The text to split. * @param {string} query - The active search query. * @param {boolean} [isActiveMatch] - Whether this message is the focused match. * @returns {Array} */ _renderHighlightedSegments(text: string, query: string, isActiveMatch?: boolean) { return getHighlightSegments(text, query).map((segment, index) => segment.match ? ( { segment.text } ) : segment.text); } /** * Renders the message privacy notice, if necessary. * * @returns {React.ReactElement<*> | null} */ _renderPrivateNotice() { const { message, knocking } = this.props; if (!(message.privateMessage || (message.lobbyChat && !knocking))) { return null; } return ( { getPrivateNoticeMessage(this.props.message) } ); } /** * Renders the private reply button, if necessary. * * @returns {React.ReactElement<*> | null} */ _renderPrivateReplyButton() { const { message, canReply } = this.props; const { lobbyChat } = message; if (!canReply) { return null; } return ( ); } /** * Renders the time at which the message was sent, if necessary. * * @returns {React.ReactElement<*> | null} */ _renderTimestamp() { if (!this.props.showTimestamp) { return null; } return ( { getFormattedTimestamp(this.props.message) } ); } } /** * Maps part of the redux state to the props of this component. * * @param {Object} state - The Redux state. * @param {IChatMessageProps} message - Message object. * @returns {IProps} */ function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) { const activeMatch = getActiveChatSearchMatch(state); return { canReply: getCanReplyToMessage(state, message), gifEnabled: isGifEnabled(state), highlightQuery: getChatSearchQuery(state), isActiveMatch: Boolean(activeMatch && activeMatch.messageId === message.messageId), knocking: state['features/lobby'].knocking }; } export default translate(connect(_mapStateToProps)(ChatMessage));