// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Dimensions, Platform, TouchableHighlight, TouchableOpacity, View, } from 'react-native'; import {intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; import {Posts} from 'mattermost-redux/constants'; import CombinedSystemMessage from 'app/components/combined_system_message'; import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; import OptionsContext from 'app/components/options_context'; import ShowMoreButton from 'app/components/show_more_button'; import {emptyFunction} from 'app/utils/general'; import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; let FileAttachmentList; let PostAddChannelMember; let PostBodyAdditionalContent; let Reactions; export default class PostBody extends PureComponent { static propTypes = { actions: PropTypes.shape({ flagPost: PropTypes.func.isRequired, unflagPost: PropTypes.func.isRequired, }).isRequired, canAddReaction: PropTypes.bool, canDelete: PropTypes.bool, canEdit: PropTypes.bool, canEditUntil: PropTypes.number.isRequired, channelIsReadOnly: PropTypes.bool.isRequired, fileIds: PropTypes.array, hasBeenDeleted: PropTypes.bool, hasBeenEdited: PropTypes.bool, hasReactions: PropTypes.bool, highlight: PropTypes.bool, isFailed: PropTypes.bool, isFlagged: PropTypes.bool, isPending: PropTypes.bool, isPostAddChannelMember: PropTypes.bool, isPostEphemeral: PropTypes.bool, isReplyPost: PropTypes.bool, isSearchResult: PropTypes.bool, isSystemMessage: PropTypes.bool, managedConfig: PropTypes.object, message: PropTypes.string, navigator: PropTypes.object.isRequired, onAddReaction: PropTypes.func, onCopyPermalink: PropTypes.func, onCopyText: PropTypes.func, onFailedPostPress: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, onPostDelete: PropTypes.func, onPostEdit: PropTypes.func, onPress: PropTypes.func, postId: PropTypes.string.isRequired, postProps: PropTypes.object, postType: PropTypes.string, replyBarStyle: PropTypes.array, showAddReaction: PropTypes.bool, showLongPost: PropTypes.bool.isRequired, theme: PropTypes.object, toggleSelected: PropTypes.func, }; static defaultProps = { fileIds: [], onAddReaction: emptyFunction, onCopyPermalink: emptyFunction, onCopyText: emptyFunction, onFailedPostPress: emptyFunction, onPostDelete: emptyFunction, onPostEdit: emptyFunction, onPress: emptyFunction, replyBarStyle: [], toggleSelected: emptyFunction, }; static contextTypes = { intl: intlShape.isRequired, }; state = { isLongPost: false, }; flagPost = () => { const {actions, postId} = this.props; actions.flagPost(postId); }; handleHideUnderlay = () => { this.props.toggleSelected(false); }; handleShowUnderlay = () => { this.props.toggleSelected(true); }; hideOptionsContext = () => { if (Platform.OS === 'ios' && this.refs.options) { this.refs.options.hide(); } }; getPostActions = () => { const {formatMessage} = this.context.intl; const { canEdit, canEditUntil, canDelete, canAddReaction, channelIsReadOnly, hasBeenDeleted, isPending, isFailed, isFlagged, isPostEphemeral, isSystemMessage, managedConfig, onCopyText, onPostDelete, onPostEdit, showAddReaction, } = this.props; const actions = []; const isPendingOrFailedPost = isPending || isFailed; // we should check for the user roles and permissions if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) { if (showAddReaction && canAddReaction && !channelIsReadOnly) { actions.push({ text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), onPress: this.props.onAddReaction, }); } if (managedConfig.copyAndPasteProtection !== 'true') { actions.push({ text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}), onPress: onCopyText, copyPost: true, }); } if (!channelIsReadOnly) { if (isFlagged) { actions.push({ text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}), onPress: this.unflagPost, }); } else { actions.push({ text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}), onPress: this.flagPost, }); } } if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) { actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit}); } actions.push({ text: formatMessage({id: 'get_post_link_modal.title', defaultMessage: 'Copy Permalink'}), onPress: this.props.onCopyPermalink, }); } if (!isPendingOrFailedPost && !isPostEphemeral && canDelete && !hasBeenDeleted) { actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete}); } return actions; }; measurePost = (event) => { const {height} = event.nativeEvent.layout; const {height: deviceHeight} = Dimensions.get('window'); const {showLongPost} = this.props; if (!showLongPost && height >= (deviceHeight * 1.2)) { this.setState({ isLongPost: true, maxHeight: (deviceHeight * 0.6), }); } }; openLongPost = preventDoubleTap(() => { const { managedConfig, navigator, onAddReaction, onHashtagPress, onPermalinkPress, postId, } = this.props; const options = { screen: 'LongPost', animationType: 'none', backButtonTitle: '', overrideBackPress: true, navigatorStyle: { navBarHidden: true, screenBackgroundColor: changeOpacity('#000', 0.2), modalPresentationStyle: 'overCurrentContext', }, passProps: { postId, managedConfig, onAddReaction, onHashtagPress, onPermalinkPress, }, }; navigator.showModal(options); }); unflagPost = () => { const {actions, postId} = this.props; actions.unflagPost(postId); }; showOptionsContext = (additionalAction) => { if (this.refs.options) { this.refs.options.show(additionalAction); } }; renderAddChannelMember = (style, messageStyle, textStyles) => { const {onPress, postProps} = this.props; if (!PostAddChannelMember) { PostAddChannelMember = require('app/components/post_add_channel_member').default; } return ( ); }; renderFileAttachments() { const { fileIds, isFailed, navigator, postId, showLongPost, toggleSelected, } = this.props; if (showLongPost) { return null; } let attachments; if (fileIds.length > 0) { if (!FileAttachmentList) { FileAttachmentList = require('app/components/file_attachment_list').default; } attachments = ( ); } return attachments; } renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => { const {isReplyPost, message, navigator, onHashtagPress, onPermalinkPress, postId, postProps} = this.props; if (!PostBodyAdditionalContent) { PostBodyAdditionalContent = require('app/components/post_body_additional_content').default; } return ( ); }; renderReactions = () => { const { hasReactions, isSearchResult, navigator, onAddReaction, postId, showLongPost, } = this.props; if (!hasReactions || isSearchResult || showLongPost) { return null; } if (!Reactions) { Reactions = require('app/components/reactions').default; } return ( ); }; render() { const {formatMessage} = this.context.intl; const { hasBeenDeleted, hasBeenEdited, highlight, isFailed, isPending, isPostAddChannelMember, isReplyPost, isSearchResult, isSystemMessage, message, navigator, onFailedPostPress, onHashtagPress, onPermalinkPress, onPress, postProps, postType, replyBarStyle, theme, toggleSelected, } = this.props; const {isLongPost, maxHeight} = this.state; const style = getStyleSheet(theme); const blockStyles = getMarkdownBlockStyles(theme); const textStyles = getMarkdownTextStyles(theme); const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message; const isPendingOrFailedPost = isPending || isFailed; let body; let messageComponent; if (hasBeenDeleted) { messageComponent = ( ); body = ({messageComponent}); } else if (isPostAddChannelMember) { messageComponent = this.renderAddChannelMember(style, messageStyle, textStyles); } else if (postType === Posts.POST_TYPES.COMBINED_USER_ACTIVITY) { const {allUserIds, allUsernames, messageData} = postProps.user_activity; messageComponent = ( ); } else if (message.length) { messageComponent = ( ); } if (!hasBeenDeleted) { body = ( {messageComponent} {isLongPost && } {this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)} {this.renderFileAttachments()} {this.renderReactions()} ); } return ( {body} {isFailed && } ); } } const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { flex: { flex: 1, }, row: { flexDirection: 'row', }, messageBody: { paddingBottom: 2, paddingTop: 2, }, retry: { justifyContent: 'center', marginLeft: 12, }, message: { color: theme.centerChannelColor, fontSize: 15, lineHeight: 20, }, messageContainerWithReplyBar: { flexDirection: 'row', flex: 1, }, pendingPost: { opacity: 0.5, }, systemMessage: { opacity: 0.6, }, }; });