import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, ViewPropTypes, TouchableWithoutFeedback
} from 'react-native';
import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input';
import {
BorderlessButton
} from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
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';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_DANGER } from '../../constants/colors';
const SYSTEM_MESSAGES = [
'r',
'au',
'ru',
'ul',
'uj',
'ut',
'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',
'message_snippeted',
'thread-created'
];
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 === 'ut') {
return I18n.t('Has_joined_the_conversation');
} 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 });
} else if (type === 'message_snippeted') {
return I18n.t('Created_snippet');
}
return '';
};
const BUTTON_HIT_SLOP = {
top: 4, right: 4, bottom: 4, left: 4
};
export default class Message extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired,
customThreadTimeFormat: PropTypes.string,
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.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string
]),
edited: PropTypes.bool,
attachments: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
urls: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
useRealName: PropTypes.bool,
dcount: PropTypes.number,
dlm: PropTypes.instanceOf(Date),
tmid: PropTypes.string,
tcount: PropTypes.number,
tlm: PropTypes.instanceOf(Date),
tmsg: PropTypes.string,
// methods
closeReactions: PropTypes.func,
onErrorPress: PropTypes.func,
onLongPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
onThreadPress: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
}
static defaultProps = {
archived: false,
broadcast: false,
attachments: [],
urls: [],
reactions: [],
onLongPress: () => {}
}
onPress = () => {
KeyboardUtils.dismiss();
}
onLongPress = () => {
const { archived, onLongPress } = this.props;
if (this.isInfoMessage() || this.hasError() || archived) {
return;
}
onLongPress();
}
formatLastMessage = (lm) => {
const { customThreadTimeFormat } = this.props;
if (customThreadTimeFormat) {
return moment(lm).format(customThreadTimeFormat);
}
return lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
}
formatMessageCount = (count, type) => {
const discussion = type === 'discussion';
let text = discussion ? I18n.t('No_messages_yet') : null;
if (count === 1) {
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
} else if (count > 1 && count < 1000) {
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
} else if (count > 999) {
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
}
return text;
}
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, user
} = this.props;
if (header) {
return (
);
}
return null;
}
renderUsername = () => {
const {
header, timeFormat, author, alias, ts, useRealName
} = this.props;
if (header) {
return (
);
}
return null;
}
renderContent() {
if (this.isInfoMessage()) {
return {getInfoMessage({ ...this.props })};
}
const {
customEmojis, msg, baseUrl, user, edited
} = this.props;
return ;
}
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 ;
}
if (file.audio_url) {
return ;
}
if (file.video_url) {
return ;
}
// eslint-disable-next-line react/no-array-index-key
return ;
});
}
renderUrl = () => {
const { urls } = this.props;
if (urls.length === 0) {
return null;
}
return urls.map((url, index) => (
));
}
renderError = () => {
if (!this.hasError()) {
return null;
}
const { onErrorPress } = this.props;
return (
);
}
renderReaction = (reaction) => {
const {
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
} = this.props;
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
return (
onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
{ reaction.usernames.length }
);
}
renderReactions() {
const { reactions, toggleReactionPicker } = this.props;
if (reactions.length === 0) {
return null;
}
return (
{reactions.map(this.renderReaction)}
);
}
renderBroadcastReply() {
const { broadcast, replyBroadcast } = this.props;
if (broadcast && !this.isOwn()) {
return (
{I18n.t('Reply')}
);
}
return null;
}
renderDiscussion = () => {
const {
msg, dcount, dlm, onDiscussionPress
} = this.props;
const time = this.formatLastMessage(dlm);
const buttonText = this.formatMessageCount(dcount, 'discussion');
return (
{I18n.t('Started_discussion')}
{msg}
{buttonText}
{time}
);
}
renderThread = () => {
const {
tcount, tlm, onThreadPress, msg
} = this.props;
if (!tlm) {
return null;
}
const time = this.formatLastMessage(tlm);
const buttonText = this.formatMessageCount(tcount, 'thread');
return (
{buttonText}
{time}
);
}
renderRepliedThread = () => {
const {
tmid, tmsg, header, onThreadPress, fetchThreadName
} = this.props;
if (!tmid || !header || this.isTemp()) {
return null;
}
if (!tmsg) {
fetchThreadName(tmid);
return null;
}
return (
{I18n.t('Replied_on')} {tmsg}
);
}
renderInner = () => {
const { type } = this.props;
if (type === 'discussion-created') {
return (
{this.renderUsername()}
{this.renderDiscussion()}
);
}
return (
{this.renderUsername()}
{this.renderRepliedThread()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderThread()}
{this.renderReactions()}
{this.renderBroadcastReply()}
);
}
render() {
const {
editing, style, header, 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 (
{this.renderError()}
{this.renderAvatar()}
{this.renderInner()}
{reactionsModal
? (
)
: null
}
);
}
}