import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { View, Text, ViewPropTypes, Image as ImageRN } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; import { KeyboardUtils } from 'react-native-keyboard-input'; import { State, RectButton, LongPressGestureHandler } from 'react-native-gesture-handler'; import Image from './Image'; import User from './User'; import Avatar from '../Avatar'; import Audio from './Audio'; import Video from './Video'; import Markdown from './Markdown'; import Url from './Url'; import Reply from './Reply'; import ReactionsModal from './ReactionsModal'; import Emoji from './Emoji'; import styles from './styles'; import I18n from '../../i18n'; import messagesStatus from '../../constants/messagesStatus'; const SYSTEM_MESSAGES = [ 'r', 'au', 'ru', 'ul', 'uj', 'rm', 'user-muted', 'user-unmuted', 'message_pinned', 'subscription-role-added', 'subscription-role-removed', 'room_changed_description', 'room_changed_announcement', 'room_changed_topic', 'room_changed_privacy' ]; const getInfoMessage = ({ type, role, msg, author }) => { const { username } = author; if (type === 'rm') { return I18n.t('Message_removed'); } else if (type === 'uj') { return I18n.t('Has_joined_the_channel'); } else if (type === 'r') { return I18n.t('Room_name_changed', { name: msg, userBy: username }); } else if (type === 'message_pinned') { return I18n.t('Message_pinned'); } else if (type === 'ul') { return I18n.t('Has_left_the_channel'); } else if (type === 'ru') { return I18n.t('User_removed_by', { userRemoved: msg, userBy: username }); } else if (type === 'au') { return I18n.t('User_added_by', { userAdded: msg, userBy: username }); } else if (type === 'user-muted') { return I18n.t('User_muted_by', { userMuted: msg, userBy: username }); } else if (type === 'user-unmuted') { return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username }); } else if (type === 'subscription-role-added') { return `${ msg } was set ${ role } by ${ username }`; } else if (type === 'subscription-role-removed') { return `${ msg } is no longer ${ role } by ${ username }`; } else if (type === 'room_changed_description') { return I18n.t('Room_changed_description', { description: msg, userBy: username }); } else if (type === 'room_changed_announcement') { return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username }); } else if (type === 'room_changed_topic') { return I18n.t('Room_changed_topic', { topic: msg, userBy: username }); } else if (type === 'room_changed_privacy') { return I18n.t('Room_changed_privacy', { type: msg, userBy: username }); } return ''; }; export default class Message extends PureComponent { static propTypes = { baseUrl: PropTypes.string.isRequired, customEmojis: PropTypes.object.isRequired, timeFormat: PropTypes.string.isRequired, msg: PropTypes.string, user: PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, token: PropTypes.string.isRequired }), author: PropTypes.shape({ _id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, name: PropTypes.string }), status: PropTypes.any, reactions: PropTypes.any, editing: PropTypes.bool, style: ViewPropTypes.style, archived: PropTypes.bool, broadcast: PropTypes.bool, reactionsModal: PropTypes.bool, type: PropTypes.string, header: PropTypes.bool, avatar: PropTypes.string, alias: PropTypes.string, ts: PropTypes.instanceOf(Date), edited: PropTypes.bool, attachments: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]), urls: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]), useRealName: PropTypes.bool, // methods closeReactions: PropTypes.func, onErrorPress: PropTypes.func, onLongPress: PropTypes.func, onReactionLongPress: PropTypes.func, onReactionPress: PropTypes.func, replyBroadcast: PropTypes.func, toggleReactionPicker: PropTypes.func } static defaultProps = { archived: false, broadcast: false, attachments: [], urls: [], reactions: [], onLongPress: () => {} } onPress = () => { KeyboardUtils.dismiss(); } isInfoMessage = () => { const { type } = this.props; return SYSTEM_MESSAGES.includes(type); } isOwn = () => { const { author, user } = this.props; return author._id === user.id; } isDeleted() { const { type } = this.props; return type === 'rm'; } isTemp() { const { status } = this.props; return status === messagesStatus.TEMP || status === messagesStatus.ERROR; } hasError() { const { status } = this.props; return status === messagesStatus.ERROR; } renderAvatar = () => { const { header, avatar, author, baseUrl } = this.props; if (header) { return ( <Avatar style={styles.avatar} text={avatar ? '' : author.username} size={36} borderRadius={4} avatar={avatar} baseUrl={baseUrl} /> ); } return null; } renderUsername = () => { const { header, timeFormat, author, alias, ts, useRealName } = this.props; if (header) { return ( <User onPress={this._onPress} timeFormat={timeFormat} username={(useRealName && author.name) || author.username} alias={alias} ts={ts} temp={this.isTemp()} /> ); } return null; } renderContent() { if (this.isInfoMessage()) { return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>; } const { customEmojis, msg, baseUrl, user, edited } = this.props; return <Markdown msg={msg} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} edited={edited} />; } renderAttachment() { const { attachments, timeFormat } = this.props; if (attachments.length === 0) { return null; } return attachments.map((file, index) => { const { user, baseUrl, customEmojis } = this.props; if (file.image_url) { return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />; } if (file.audio_url) { return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />; } if (file.video_url) { return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />; } // eslint-disable-next-line react/no-array-index-key return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />; }); } renderUrl = () => { const { urls } = this.props; if (urls.length === 0) { return null; } return urls.map((url, index) => ( <Url url={url} key={url.url} index={index} /> )); } renderError = () => { if (!this.hasError()) { return null; } const { onErrorPress } = this.props; return <Icon name='error-outline' color='red' size={20} style={styles.errorIcon} onPress={onErrorPress} />; } renderReaction = (reaction) => { const { user, onReactionLongPress, onReactionPress, customEmojis, baseUrl } = this.props; const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1; const underlayColor = reacted ? '#fff' : '#e1e5e8'; return ( <LongPressGestureHandler key={reaction.emoji} onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()} > <RectButton onPress={() => onReactionPress(reaction.emoji)} testID={`message-reaction-${ reaction.emoji }`} style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]} activeOpacity={0.8} underlayColor={underlayColor} > <View style={[styles.reactionContainer, reacted && styles.reactedContainer]}> <Emoji content={reaction.emoji} customEmojis={customEmojis} standardEmojiStyle={styles.reactionEmoji} customEmojiStyle={styles.reactionCustomEmoji} baseUrl={baseUrl} /> <Text style={styles.reactionCount}>{ reaction.usernames.length }</Text> </View> </RectButton> </LongPressGestureHandler> ); } renderReactions() { const { reactions, toggleReactionPicker } = this.props; if (reactions.length === 0) { return null; } return ( <View style={styles.reactionsContainer}> {reactions.map(this.renderReaction)} <RectButton onPress={toggleReactionPicker} key='message-add-reaction' testID='message-add-reaction' style={styles.reactionButton} activeOpacity={0.8} underlayColor='#e1e5e8' > <View style={styles.reactionContainer}> <ImageRN source={{ uri: 'add_reaction' }} style={styles.addReaction} /> </View> </RectButton> </View> ); } renderBroadcastReply() { const { broadcast, replyBroadcast } = this.props; if (broadcast && !this.isOwn()) { return ( <RectButton onPress={replyBroadcast} style={styles.broadcastButton} activeOpacity={0.5} underlayColor='#fff' > <ImageRN source={{ uri: 'reply' }} style={styles.broadcastButtonIcon} /> <Text style={styles.broadcastButtonText}>{I18n.t('Reply')}</Text> </RectButton> ); } return null; } render() { const { editing, style, header, archived, onLongPress, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl } = this.props; const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg }); return ( <LongPressGestureHandler onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onLongPress()} > <RectButton enabled={!(this.isInfoMessage() || this.hasError() || archived)} style={[styles.container, header && { marginBottom: 10 }]} onPress={this.onPress} activeOpacity={0.8} underlayColor='#e1e5e8' > <View style={[styles.message, editing && styles.editing, style]} accessibilityLabel={accessibilityLabel} > <View style={styles.flex}> {this.renderError()} {this.renderAvatar()} <View style={[styles.messageContent, header && styles.hasHeader, this.isTemp() && styles.temp]}> {this.renderUsername()} {this.renderContent()} {this.renderAttachment()} {this.renderUrl()} {this.renderReactions()} {this.renderBroadcastReply()} </View> </View> {reactionsModal ? ( <ReactionsModal isVisible={reactionsModal} reactions={reactions} user={user} customEmojis={customEmojis} baseUrl={baseUrl} close={closeReactions} /> ) : null } </View> </RectButton> </LongPressGestureHandler> ); } }