[MM-9516] Add feature to add user to channel after at-mention (#1521)

* add feature to add user to channel after at-mention

* update per comment and update mattermost-redux

* move sendAddToChannelEphemeralPost to actions folder

* rebase and fix merge conflicts, do clean up a bit
This commit is contained in:
Saturnino Abril 2018-03-27 01:20:13 +08:00 committed by Harrison Healey
parent 217c98cab6
commit 8531212060
7 changed files with 347 additions and 13 deletions

39
app/actions/views/post.js Normal file
View file

@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
import {generateId} from 'app/utils/file';
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
return async (dispatch) => {
const timestamp = Date.now();
const post = {
id: generateId(),
user_id: user.id,
channel_id: channelId,
message,
type: Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL,
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
parent_id: postRootId,
props: {
username: user.username,
addedUsername,
},
};
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post,
},
},
channelId,
});
};
}

View file

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addChannelMember} from 'mattermost-redux/actions/channels';
import {removePost} from 'mattermost-redux/actions/posts';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {sendAddToChannelEphemeralPost} from 'app/actions/views/post';
import PostAddChannelMember from './post_add_channel_member';
function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId) || {};
let channelType = '';
if (post && post.channel_id) {
const channel = getChannel(state, post.channel_id);
if (channel && channel.type) {
channelType = channel.type;
}
}
return {
channelType,
currentUser: getCurrentUser(state),
post,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addChannelMember,
removePost,
sendAddToChannelEphemeralPost,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PostAddChannelMember);

View file

@ -0,0 +1,189 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import {General} from 'mattermost-redux/constants';
import {concatStyles} from 'app/utils/theme';
import AtMention from 'app/components/at_mention';
import FormattedText from 'app/components/formatted_text';
export default class PostAddChannelMember extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
addChannelMember: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
sendAddToChannelEphemeralPost: PropTypes.func.isRequired,
}).isRequired,
currentUser: PropTypes.object.isRequired,
channelType: PropTypes.string,
post: PropTypes.object.isRequired,
postId: PropTypes.string.isRequired,
userIds: PropTypes.array.isRequired,
usernames: PropTypes.array.isRequired,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func,
onPostPress: PropTypes.func,
textStyles: PropTypes.object,
};
static contextTypes = {
intl: intlShape,
};
computeTextStyle = (baseStyle, context) => {
return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type]));
}
handleAddChannelMember = () => {
const {
actions,
currentUser,
post,
userIds,
usernames,
} = this.props;
const {formatMessage} = this.context.intl;
if (post && post.channel_id) {
userIds.forEach((userId, index) => {
actions.addChannelMember(post.channel_id, userId);
if (post.root_id) {
const message = formatMessage(
{
id: 'api.channel.add_member.added',
defaultMessage: '{addedUsername} added to the channel by {username}.',
},
{
username: currentUser.username,
addedUsername: usernames[index],
}
);
actions.sendAddToChannelEphemeralPost(currentUser, usernames[index], message, post.channel_id, post.root_id);
}
});
actions.removePost(post);
}
}
generateAtMentions(usernames = []) {
if (usernames.length === 1) {
return (
<AtMention
mentionStyle={this.props.textStyles.mention}
mentionName={usernames[0]}
onLongPress={this.props.onLongPress}
onPostPress={this.props.onPostPress}
navigator={this.props.navigator}
/>
);
} else if (usernames.length > 1) {
function andSeparator(key) {
return (
<FormattedText
key={key}
id={'post_body.check_for_out_of_channel_mentions.link.and'}
defaultMessage={' and '}
/>
);
}
function commaSeparator(key) {
return <Text key={key}>{', '}</Text>;
}
return (
<Text>
{
usernames.map((username) => {
return (
<AtMention
key={username}
mentionStyle={this.props.textStyles.mention}
mentionName={username}
onLongPress={this.props.onLongPress}
onPostPress={this.props.onPostPress}
navigator={this.props.navigator}
/>
);
}).reduce((acc, el, idx, arr) => {
if (idx === 0) {
return [el];
} else if (idx === arr.length - 1) {
return [...acc, andSeparator(idx), el];
}
return [...acc, commaSeparator(idx), el];
}, [])
}
</Text>
);
}
return '';
}
render() {
const {channelType, postId, usernames} = this.props;
if (!postId || !channelType) {
return null;
}
let linkId;
let linkText;
if (channelType === General.PRIVATE_CHANNEL) {
linkId = 'post_body.check_for_out_of_channel_mentions.link.private';
linkText = 'add them to this private channel';
} else if (channelType === General.OPEN_CHANNEL) {
linkId = 'post_body.check_for_out_of_channel_mentions.link.public';
linkText = 'add them to the channel';
}
let messageId;
let messageText;
if (usernames.length === 1) {
messageId = 'post_body.check_for_out_of_channel_mentions.message.one';
messageText = 'was mentioned but is not in the channel. Would you like to ';
} else if (usernames.length > 1) {
messageId = 'post_body.check_for_out_of_channel_mentions.message.multiple';
messageText = 'were mentioned but they are not in the channel. Would you like to ';
}
const atMentions = this.generateAtMentions(usernames);
return (
<Text>
{atMentions}
{' '}
<FormattedText
id={messageId}
defaultMessage={messageText}
/>
<Text
style={this.props.textStyles.link}
id='add_channel_member_link'
onPress={this.handleAddChannelMember}
>
<FormattedText
id={linkId}
defaultMessage={linkText}
/>
</Text>
<FormattedText
id={'post_body.check_for_out_of_channel_mentions.message_last'}
defaultMessage={'? They will have access to all message history.'}
/>
</Text>
);
}
}

View file

@ -5,10 +5,25 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
import {
General,
Posts,
} from 'mattermost-redux/constants';
import {
getCurrentChannel,
getMyCurrentChannelMembership,
} from 'mattermost-redux/selectors/entities/channels';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isEdited, isPostEphemeral, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {canManageMembers} from 'mattermost-redux/utils/channel_utils';
import {
isEdited,
isPostEphemeral,
isSystemMessage,
} from 'mattermost-redux/utils/post_utils';
import PostBody from './post_body';
@ -26,6 +41,25 @@ function mapStateToProps(state, ownProps) {
isPending = false;
}
const channel = getCurrentChannel(state);
const user = getCurrentUser(state);
const teamMember = getCurrentTeamMembership(state);
const channelMember = getMyCurrentChannelMembership(state);
const {config, license} = state.entities.general;
const isUserCanManageMembers = canManageMembers(channel, user, teamMember, channelMember, config, license);
const isEphemeralPost = isPostEphemeral(post);
let isPostAddChannelMember = false;
if (
(channel.type === General.PRIVATE_CHANNEL || channel.type === General.OPEN_CHANNEL) &&
isUserCanManageMembers &&
isEphemeralPost &&
post.props &&
post.props.add_channel_member
) {
isPostAddChannelMember = true;
}
return {
postProps: post.props || {},
fileIds: post.file_ids,
@ -34,7 +68,8 @@ function mapStateToProps(state, ownProps) {
hasReactions: post.has_reactions,
isFailed,
isPending,
isPostEphemeral: isPostEphemeral(post),
isPostAddChannelMember,
isPostEphemeral: isEphemeralPost,
isSystemMessage: isSystemMessage(post),
message: post.message,
theme: getTheme(state),

View file

@ -19,6 +19,7 @@ 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';
@ -44,6 +45,7 @@ export default class PostBody extends PureComponent {
isFailed: PropTypes.bool,
isFlagged: PropTypes.bool,
isPending: PropTypes.bool,
isPostAddChannelMember: PropTypes.bool,
isPostEphemeral: PropTypes.bool,
isReplyPost: PropTypes.bool,
isSearchResult: PropTypes.bool,
@ -344,6 +346,7 @@ export default class PostBody extends PureComponent {
hasBeenEdited,
isFailed,
isPending,
isPostAddChannelMember,
isSearchResult,
isSystemMessage,
message,
@ -351,6 +354,7 @@ export default class PostBody extends PureComponent {
onFailedPostPress,
onPermalinkPress,
onPress,
postProps,
renderReplyBar,
theme,
toggleSelected,
@ -382,6 +386,23 @@ export default class PostBody extends PureComponent {
</TouchableHighlight>
);
body = (<View>{messageComponent}</View>);
} else if (isPostAddChannelMember) {
messageComponent = (
<View style={style.row}>
<View style={style.flex}>
<PostAddChannelMember
navigator={navigator}
onLongPress={this.showOptionsContext}
onPermalinkPress={onPermalinkPress}
onPostPress={onPress}
textStyles={textStyles}
postId={postProps.add_channel_member.post_id}
userIds={postProps.add_channel_member.user_ids}
usernames={postProps.add_channel_member.usernames}
/>
</View>
</View>
);
} else if (message.length) {
messageComponent = (
<View style={style.row}>

View file

@ -24,11 +24,12 @@ export function makePreparePostIdsForPostList() {
return createIdsSelector(
(state, props) => getMyPosts(state, props.postIds),
(state) => state.entities.posts.selectedPostId,
(state, props) => props.lastViewedAt,
(state, props) => props.indicateNewMessages,
getCurrentUser,
shouldShowJoinLeaveMessages,
(posts, lastViewedAt, indicateNewMessages, currentUser, showJoinLeave) => {
(posts, selectedPostId, lastViewedAt, indicateNewMessages, currentUser, showJoinLeave) => {
if (posts.length === 0 || !currentUser) {
return [];
}
@ -42,6 +43,10 @@ export function makePreparePostIdsForPostList() {
for (let i = posts.length - 1; i >= 0; i--) {
const post = posts[i];
if (post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !selectedPostId) {
continue;
}
if (post.state === Posts.POST_DELETED && post.user_id === currentUser.id) {
continue;
}

View file

@ -21,7 +21,7 @@ describe('Selectors.PostList', () => {
entities: {
posts: {
posts: {
1001: {id: '1001', create_at: 0},
1001: {id: '1001', create_at: 0, type: ''},
1002: {id: '1002', create_at: 1, type: Posts.POST_TYPES.JOIN_CHANNEL},
},
},
@ -115,9 +115,9 @@ describe('Selectors.PostList', () => {
entities: {
posts: {
posts: {
1000: {id: '1000', create_at: 1000},
1005: {id: '1005', create_at: 1005},
1010: {id: '1010', create_at: 1010},
1000: {id: '1000', create_at: 1000, type: ''},
1005: {id: '1005', create_at: 1005, type: ''},
1010: {id: '1010', create_at: 1010, type: ''},
},
},
preferences: {
@ -152,11 +152,11 @@ describe('Selectors.PostList', () => {
// Posts 7 hours apart so they should appear on multiple days
const initialPosts = {
1001: {id: '1001', create_at: 1 * 60 * 60 * 1000},
1002: {id: '1002', create_at: (1 * 60 * 60 * 1000) + 5},
1003: {id: '1003', create_at: (1 * 60 * 60 * 1000) + 10},
1004: {id: '1004', create_at: 25 * 60 * 60 * 1000},
1005: {id: '1005', create_at: (25 * 60 * 60 * 1000) + 5},
1001: {id: '1001', create_at: 1 * 60 * 60 * 1000, type: ''},
1002: {id: '1002', create_at: (1 * 60 * 60 * 1000) + 5, type: ''},
1003: {id: '1003', create_at: (1 * 60 * 60 * 1000) + 10, type: ''},
1004: {id: '1004', create_at: 25 * 60 * 60 * 1000, type: ''},
1005: {id: '1005', create_at: (25 * 60 * 60 * 1000) + 5, type: ''},
1006: {id: '1006', create_at: (25 * 60 * 60 * 1000) + 10, type: Posts.POST_TYPES.JOIN_CHANNEL},
};
let state = {