// Copyright (c) 2017-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, Text, TouchableHighlight, TouchableOpacity, View, } from 'react-native'; import {intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; import LinearGradient from 'react-native-linear-gradient'; import FileAttachmentList from 'app/components/file_attachment_list'; import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; import OptionsContext from 'app/components/options_context'; import PostAddChannelMember from 'app/components/post_add_channel_member'; import PostBodyAdditionalContent from 'app/components/post_body_additional_content'; 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'; import Reactions from 'app/components/reactions'; export default class PostBody extends PureComponent { static propTypes = { actions: PropTypes.shape({ flagPost: PropTypes.func.isRequired, unflagPost: PropTypes.func.isRequired, }).isRequired, canDelete: PropTypes.bool, canEdit: PropTypes.bool, 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, onPermalinkPress: PropTypes.func, onPostDelete: PropTypes.func, onPostEdit: PropTypes.func, onPress: PropTypes.func, postId: PropTypes.string.isRequired, postProps: PropTypes.object, renderReplyBar: PropTypes.func, 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, renderReplyBar: emptyFunction, 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, canDelete, hasBeenDeleted, isPending, isFailed, isFlagged, isPostEphemeral, isSystemMessage, managedConfig, onCopyText, onPostDelete, onPostEdit, } = this.props; const actions = []; const isPendingOrFailedPost = isPending || isFailed; // we should check for the user roles and permissions if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) { 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 (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) { actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit}); } if (canDelete && !hasBeenDeleted) { actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete}); } actions.push({ text: formatMessage({id: 'get_post_link_modal.title', defaultMessage: 'Copy Permalink'}), onPress: this.props.onCopyPermalink, }); } return actions; }; measurePost = (event) => { const {height} = event.nativeEvent.layout; const {height: deviceHeight} = Dimensions.get('window'); const {showLongPost} = this.props; if (!showLongPost && height >= 1000) { this.setState({ isLongPost: true, maxHeight: (deviceHeight * 0.6), }); } }; openLongPost = preventDoubleTap(() => { const {managedConfig, navigator, onAddReaction, 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, onPermalinkPress, }, }; navigator.showModal(options); }); unflagPost = () => { const {actions, postId} = this.props; actions.unflagPost(postId); }; showOptionsContext = (additionalAction) => { if (this.refs.options) { this.refs.options.show(additionalAction); } }; renderFileAttachments() { const { fileIds, isFailed, navigator, onPress, postId, showLongPost, toggleSelected, } = this.props; if (showLongPost) { return null; } let attachments; if (fileIds.length > 0) { attachments = ( ); } return attachments; } renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => { const {isReplyPost, message, navigator, onPermalinkPress, postId, postProps} = this.props; return ( ); }; renderReactions = () => { const {hasReactions, isSearchResult, postId, onAddReaction, showLongPost} = this.props; if (!hasReactions || isSearchResult || showLongPost) { return null; } return ( ); }; renderShowMoreOption = (style) => { const {highlight, theme} = this.props; const {isLongPost} = this.state; if (!isLongPost) { return null; } const gradientColors = []; if (highlight) { gradientColors.push( changeOpacity(theme.mentionHighlightBg, 0), changeOpacity(theme.mentionHighlightBg, 0.15), changeOpacity(theme.mentionHighlightBg, 0.5), ); } else { gradientColors.push( changeOpacity(theme.centerChannelBg, 0), changeOpacity(theme.centerChannelBg, 0.75), theme.centerChannelBg, ); } return ( {'+'} ); }; render() { const {formatMessage} = this.context.intl; const { hasBeenDeleted, hasBeenEdited, isFailed, isPending, isPostAddChannelMember, isSearchResult, isSystemMessage, message, navigator, onFailedPostPress, onPermalinkPress, onPress, postProps, renderReplyBar, 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 = ( ); } else if (message.length) { messageComponent = ( ); } if (!hasBeenDeleted) { body = ( {messageComponent} {this.renderShowMoreOption(style)} {this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)} {this.renderFileAttachments()} {this.renderReactions()} ); } return ( {renderReplyBar()} {body} {isFailed && } ); } } const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { flex: { flex: 1, }, row: { flexDirection: 'row', }, 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, }, showMoreGradient: { flex: 1, height: 50, position: 'absolute', top: -50, width: '100%', }, showMoreContainer: { alignItems: 'center', justifyContent: 'center', flex: 1, flexDirection: 'row', position: 'relative', top: -7.5, }, showMoreDividerLeft: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), flex: 1, height: 1, marginRight: 10, }, showMoreDividerRight: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), flex: 1, height: 1, marginLeft: 10, }, showMoreButtonContainer: { backgroundColor: theme.centerChannelBg, borderColor: changeOpacity(theme.centerChannelColor, 0.2), borderRadius: 4, borderWidth: 1, height: 37, paddingHorizontal: 10, }, showMoreButton: { alignItems: 'center', flex: 1, flexDirection: 'row', }, showMorePlusSign: { color: theme.linkColor, fontSize: 16, fontWeight: '600', marginRight: 8, }, showMoreText: { color: theme.linkColor, fontSize: 13, fontWeight: '600', }, }; });