// 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 { Keyboard, Platform, View, ViewPropTypes, } from 'react-native'; import {intlShape} from 'react-intl'; import {Posts} from '@mm-redux/constants'; import EventEmitter from '@mm-redux/utils/event_emitter'; import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils'; import PostBody from 'app/components/post_body'; import PostHeader from 'app/components/post_header'; import PostPreHeader from 'app/components/post_header/post_pre_header'; import PostProfilePicture from 'app/components/post_profile_picture'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import {NavigationTypes} from 'app/constants'; import {fromAutoResponder} from 'app/utils/general'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation'; export default class Post extends PureComponent { static propTypes = { actions: PropTypes.shape({ createPost: PropTypes.func.isRequired, insertToDraft: PropTypes.func.isRequired, removePost: PropTypes.func.isRequired, }).isRequired, channelIsReadOnly: PropTypes.bool, currentUserId: PropTypes.string.isRequired, highlight: PropTypes.bool, style: ViewPropTypes.style, post: PropTypes.object, renderReplies: PropTypes.bool, isFirstReply: PropTypes.bool, isLastReply: PropTypes.bool, isLastPost: PropTypes.bool, consecutivePost: PropTypes.bool, hasComments: PropTypes.bool, isSearchResult: PropTypes.bool, commentedOnPost: PropTypes.object, managedConfig: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, shouldRenderReplyButton: PropTypes.bool, showAddReaction: PropTypes.bool, showFullDate: PropTypes.bool, showLongPost: PropTypes.bool, theme: PropTypes.object.isRequired, onPress: PropTypes.func, onReply: PropTypes.func, isFlagged: PropTypes.bool, highlightPinnedOrFlagged: PropTypes.bool, skipFlaggedHeader: PropTypes.bool, skipPinnedHeader: PropTypes.bool, isCommentMention: PropTypes.bool, location: PropTypes.string, isBot: PropTypes.bool, isLandscape: PropTypes.bool.isRequired, previousPostExists: PropTypes.bool, beforePrevPostUserId: PropTypes.string, }; static defaultProps = { isSearchResult: false, showAddReaction: true, showLongPost: false, channelIsReadOnly: false, highlightPinnedOrFlagged: true, }; static contextTypes = { intl: intlShape.isRequired, }; constructor(props) { super(props); this.postBodyRef = React.createRef(); } goToUserProfile = () => { const {intl} = this.context; const {post} = this.props; const screen = 'UserProfile'; const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); const passProps = { userId: post.user_id, }; Keyboard.dismiss(); requestAnimationFrame(() => { goToScreen(screen, title, passProps); }); }; autofillUserMention = (username) => { this.props.actions.insertToDraft(`@${username} `); }; handleFailedPostPress = () => { const screen = 'OptionsModal'; const passProps = { title: { id: t('mobile.post.failed_title'), defaultMessage: 'Unable to send your message:', }, items: [{ action: () => { const {failed, id, ...post} = this.props.post; // eslint-disable-line EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL); this.props.actions.createPost(post); }, text: { id: t('mobile.post.failed_retry'), defaultMessage: 'Try Again', }, }, { action: () => { EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL); this.onRemovePost(this.props.post); }, text: { id: t('mobile.post.failed_delete'), defaultMessage: 'Delete Message', }, textStyle: { color: '#CC3239', }, }], }; showModalOverCurrentContext(screen, passProps); }; handlePress = preventDoubleTap(() => { this.onPressDetected = true; const { onPress, post, showLongPost, } = this.props; const isValidSystemMessage = fromAutoResponder(post) || !isSystemMessage(post); if (onPress && post.state !== Posts.POST_DELETED && isValidSystemMessage && !isPostPendingOrFailed(post)) { onPress(post); } else if ((isPostEphemeral(post) || post.state === Posts.POST_DELETED) && !showLongPost) { this.onRemovePost(post); } setTimeout(() => { this.onPressDetected = false; }, 300); }); handleReply = preventDoubleTap(() => { const {post, onReply} = this.props; if (onReply) { return onReply(post); } return this.handlePress(); }); onRemovePost = (post) => { const {removePost} = this.props.actions; removePost(post); }; isReplyPost = () => { const {renderReplies, post} = this.props; return Boolean(renderReplies && post.root_id && (!isPostEphemeral(post) || post.state === Posts.POST_DELETED)); }; replyBarStyle = () => { const { commentedOnPost, isFirstReply, isLastReply, theme, isCommentMention, } = this.props; if (!this.isReplyPost()) { return null; } const style = getStyleSheet(theme); const replyBarStyle = [style.replyBar]; if (isFirstReply || commentedOnPost) { replyBarStyle.push(style.replyBarFirst); } if (isLastReply) { replyBarStyle.push(style.replyBarLast); } if (isCommentMention) { replyBarStyle.push(style.commentMentionBgColor); } return replyBarStyle; }; viewUserProfile = preventDoubleTap(() => { this.goToUserProfile(); }); showPostOptions = () => { if (this.postBodyRef?.current && !this.onPressDetected) { this.postBodyRef.current.showPostOptions(); } }; render() { const { channelIsReadOnly, commentedOnPost, highlight, isLastPost, isLastReply, isSearchResult, onHashtagPress, onPermalinkPress, post, isBot, renderReplies, shouldRenderReplyButton, showAddReaction, showFullDate, showLongPost, theme, managedConfig, consecutivePost, hasComments, isFlagged, highlightPinnedOrFlagged, skipFlaggedHeader, skipPinnedHeader, location, isLandscape, previousPostExists, beforePrevPostUserId, } = this.props; if (!post) { return null; } const style = getStyleSheet(theme); const isReplyPost = this.isReplyPost(); const mergeMessage = consecutivePost && !hasComments && !isBot; const highlightFlagged = isFlagged && !skipFlaggedHeader; const hightlightPinned = post.is_pinned && !skipPinnedHeader; let highlighted; if (highlight) { highlighted = style.highlight; } else if ((highlightFlagged || hightlightPinned) && highlightPinnedOrFlagged) { highlighted = style.highlightPinnedOrFlagged; } let postHeader; let userProfile; let consecutiveStyle; if (mergeMessage) { consecutiveStyle = {marginTop: 0}; userProfile = ; } else { userProfile = ( ); postHeader = ( ); } const replyBarStyle = this.replyBarStyle(); const rightColumnStyle = [style.rightColumn, (commentedOnPost && isLastReply && style.rightColumnPadding)]; return ( <> {userProfile} {postHeader} ); } } const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { postStyle: { overflow: 'hidden', flex: 1, }, container: { flexDirection: 'row', }, pendingPost: { opacity: 0.5, }, preHeaderRightColumn: { flex: 1, flexDirection: 'column', marginLeft: 2, }, rightColumn: { flex: 1, flexDirection: 'column', marginRight: 12, }, rightColumnPadding: { paddingBottom: 3, }, consecutivePostContainer: { marginBottom: 10, marginRight: 10, marginLeft: 47, marginTop: 10, }, profilePictureContainer: { marginBottom: 5, marginLeft: 12, marginTop: 10, // to compensate STATUS_BUFFER in profile_picture component ...Platform.select({ android: { marginRight: 11, }, ios: { marginRight: 10, }, }), }, replyBar: { backgroundColor: theme.centerChannelColor, opacity: 0.1, marginLeft: 1, marginRight: 7, width: 3, flexBasis: 3, }, replyBarFirst: { paddingTop: 10, }, replyBarLast: { paddingBottom: 10, }, commentMentionBgColor: { backgroundColor: theme.mentionHighlightBg, opacity: 1, }, highlight: { backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.5), }, highlightPinnedOrFlagged: { backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.2), }, }; });