[MM-10285] Merge consecutive messages by same user (#1714)

* MM-10285 show avatar and reply header only on first messages

* implemented changes for code review, thanks @koxen

* resolved merge conflicts
This commit is contained in:
Sven Hüster 2018-06-27 12:54:11 +02:00 committed by Elias Nahum
parent f6d4d40caa
commit 830022cb82
2 changed files with 146 additions and 94 deletions

View file

@ -6,83 +6,109 @@ import {bindActionCreators} from 'redux';
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {canDeletePost, canEditPost, isPostFlagged} from 'mattermost-redux/utils/post_utils';
import {canDeletePost, canEditPost, isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {addReaction} from 'app/actions/views/emoji';
import {getDimensions} from 'app/selectors/device';
import {Posts} from 'mattermost-redux/constants';
import Post from './post';
function mapStateToProps(state, ownProps) {
function isConsecutivePost(state, ownProps) {
const post = getPost(state, ownProps.postId);
const previousPost = ownProps.previousPostId && getPost(state, ownProps.previousPostId);
const config = getConfig(state);
const license = getLicense(state);
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const myPreferences = getMyPreferences(state);
const currentUserId = getCurrentUserId(state);
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
let consecutivePost = false;
let isFirstReply = true;
let isLastReply = true;
let commentedOnPost = null;
if (ownProps.renderReplies && post && post.root_id) {
if (ownProps.previousPostId) {
const previousPost = getPost(state, ownProps.previousPostId);
if (previousPost) {
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(previousPost.props && previousPost.props.from_webhook);
if (previousPost && (previousPost.id === post.root_id || previousPost.root_id === post.root_id)) {
// Previous post is root post or previous post is in same thread
isFirstReply = false;
} else {
// Last post is not a comment on the same message
commentedOnPost = getPost(state, post.root_id);
}
}
if (ownProps.nextPostId) {
const nextPost = getPost(state, ownProps.nextPostId);
if (nextPost && nextPost.root_id === post.root_id) {
isLastReply = false;
}
if (previousPost && previousPost.user_id === post.user_id &&
post.create_at - previousPost.create_at <= Posts.POST_COLLAPSE_TIMEOUT &&
!postFromWebhook && !prevPostFromWebhook &&
!isSystemMessage(post) && !isSystemMessage(previousPost) &&
previousPost.root_id === post.root_id) {
// The last post and this post were made by the same user within some time
consecutivePost = true;
}
}
return consecutivePost;
}
const {deviceWidth} = getDimensions(state);
function makeMapStateToProps() {
const getCommentCountForPost = makeGetCommentCountForPost();
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId);
const config = getConfig(state);
const license = getLicense(state);
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const myPreferences = getMyPreferences(state);
const currentUserId = getCurrentUserId(state);
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let isFirstReply = true;
let isLastReply = true;
let commentedOnPost = null;
let canDelete = false;
let canEdit = false;
if (post) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post);
}
if (ownProps.renderReplies && post && post.root_id) {
if (ownProps.previousPostId) {
const previousPost = getPost(state, ownProps.previousPostId);
if (previousPost && (previousPost.id === post.root_id || previousPost.root_id === post.root_id)) {
// Previous post is root post or previous post is in same thread
isFirstReply = false;
} else {
// Last post is not a comment on the same message
commentedOnPost = getPost(state, post.root_id);
}
}
return {
channelIsReadOnly: isCurrentChannelReadOnly(state),
config,
canDelete,
canEdit,
currentTeamUrl: getCurrentTeamUrl(state),
currentUserId,
deviceWidth,
post,
isFirstReply,
isLastReply,
commentedOnPost,
license,
theme: getTheme(state),
isFlagged: isPostFlagged(post.id, myPreferences),
if (ownProps.nextPostId) {
const nextPost = getPost(state, ownProps.nextPostId);
if (nextPost && nextPost.root_id === post.root_id) {
isLastReply = false;
}
}
}
const {deviceWidth} = getDimensions(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let canDelete = false;
let canEdit = false;
if (post) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post);
}
return {
channelIsReadOnly: isCurrentChannelReadOnly(state),
config,
canDelete,
canEdit,
currentTeamUrl: getCurrentTeamUrl(state),
currentUserId,
deviceWidth,
post,
isFirstReply,
isLastReply,
consecutivePost: isConsecutivePost(state, ownProps),
hasComments: getCommentCountForPost(state, {post}) > 0,
commentedOnPost,
license,
theme: getTheme(state),
isFlagged: isPostFlagged(post.id, myPreferences),
};
};
}
@ -99,4 +125,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Post);
export default connect(makeMapStateToProps, mapDispatchToProps)(Post);

View file

@ -51,6 +51,8 @@ export default class Post extends PureComponent {
renderReplies: PropTypes.bool,
isFirstReply: PropTypes.bool,
isLastReply: PropTypes.bool,
consecutivePost: PropTypes.bool,
hasComments: PropTypes.bool,
isSearchResult: PropTypes.bool,
commentedOnPost: PropTypes.object,
license: PropTypes.object.isRequired,
@ -139,7 +141,7 @@ export default class Post extends PureComponent {
autofillUserMention = (username) => {
this.props.actions.insertToDraft(`@${username} `);
}
};
handleEditDisable = () => {
this.setState({canEdit: false});
@ -151,7 +153,10 @@ export default class Post extends PureComponent {
Alert.alert(
formatMessage({id: 'mobile.post.delete_title', defaultMessage: 'Delete Post'}),
formatMessage({id: 'mobile.post.delete_question', defaultMessage: 'Are you sure you want to delete this post?'}),
formatMessage({
id: 'mobile.post.delete_question',
defaultMessage: 'Are you sure you want to delete this post?',
}),
[{
text: formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
style: 'cancel',
@ -194,31 +199,30 @@ export default class Post extends PureComponent {
handleAddReactionToPost = (emoji) => {
const {post} = this.props;
this.props.actions.addReaction(post.id, emoji);
}
};
handleAddReaction = preventDoubleTap(() => {
const {intl} = this.context;
const {navigator, post, theme} = this.props;
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
then((source) => {
navigator.showModal({
screen: 'AddReaction',
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
post,
closeButton: source,
onEmojiPress: this.handleAddReactionToPost,
},
});
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
navigator.showModal({
screen: 'AddReaction',
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
post,
closeButton: source,
onEmojiPress: this.handleAddReactionToPost,
},
});
});
});
handleFailedPostPress = () => {
@ -355,7 +359,7 @@ export default class Post extends PureComponent {
}
Clipboard.setString(textToCopy);
}
};
handleCopyPermalink = () => {
const {currentTeamUrl, postId} = this.props;
@ -394,6 +398,8 @@ export default class Post extends PureComponent {
showLongPost,
theme,
managedConfig,
consecutivePost,
hasComments,
isFlagged,
} = this.props;
@ -405,14 +411,19 @@ export default class Post extends PureComponent {
const selected = this.state && this.state.selected ? style.selected : null;
const highlighted = highlight ? style.highlight : null;
const isReplyPost = this.isReplyPost();
const onUsernamePress = Config.ExperimentalUsernamePressIsMention ? this.autofillUserMention : this.viewUserProfile;
const mergeMessage = consecutivePost && !hasComments;
// postWidth = deviceWidth - profilePic width - profilePictureContainer margins - right column margin
const postWidth = this.props.deviceWidth - 66;
return (
<View style={[style.container, this.props.style, highlighted, selected]}>
let postHeader;
let userProfile;
if (mergeMessage) {
userProfile = <View style={style.consecutivePostContainer}/>;
} else {
userProfile = (
<TouchableHighlight
style={[style.profilePictureContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}
onPress={this.handlePress}
@ -426,22 +437,31 @@ export default class Post extends PureComponent {
postId={post.id}
/>
</TouchableHighlight>
);
postHeader = (
<PostHeader
postId={post.id}
commentedOnUserId={commentedOnPost && commentedOnPost.user_id}
createAt={post.create_at}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
showFullDate={showFullDate}
onPress={this.handleReply}
onUsernamePress={onUsernamePress}
renderReplies={renderReplies}
theme={theme}
isFlagged={isFlagged}
/>
);
}
return (
<View style={[style.container, this.props.style, highlighted, selected]}>
{userProfile}
<View style={style.messageContainerWithReplyBar}>
{!commentedOnPost && this.renderReplyBar()}
<View style={[style.rightColumn, (commentedOnPost && isLastReply && style.rightColumnPadding)]}>
<PostHeader
postId={post.id}
commentedOnUserId={commentedOnPost && commentedOnPost.user_id}
createAt={post.create_at}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
showFullDate={showFullDate}
onPress={this.handleReply}
onUsernamePress={onUsernamePress}
renderReplies={renderReplies}
theme={theme}
isFlagged={isFlagged}
/>
{postHeader}
<View style={{maxWidth: postWidth}}>
<PostBody
ref={'postBody'}
@ -496,6 +516,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
flex: 1,
},
consecutivePostContainer: {
marginBottom: 10,
marginRight: 10,
marginLeft: 46,
marginTop: 10,
},
profilePictureContainer: {
marginBottom: 10,
marginRight: 10,