[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:
parent
217c98cab6
commit
8531212060
7 changed files with 347 additions and 13 deletions
39
app/actions/views/post.js
Normal file
39
app/actions/views/post.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
45
app/components/post_add_channel_member/index.js
Normal file
45
app/components/post_add_channel_member/index.js
Normal 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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue