[MM-10594] Add feature to combine join/add/leave/remove to channel/team system messages (#1682)

* add feature to combine system messages

Signed-off-by: Saturnino Abril <saturnino.abril@gmail.com>

* updated copyright to 2015

Signed-off-by: Saturnino Abril <saturnino.abril@gmail.com>

* updated per comments

Signed-off-by: Saturnino Abril <saturnino.abril@gmail.com>
This commit is contained in:
Saturnino Abril 2018-05-25 04:34:33 +08:00 committed by Elias Nahum
parent e72bb48d98
commit 1e4ee0716f
11 changed files with 896 additions and 0 deletions

View file

@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CombinedSystemMessage should match snapshot 1`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
>
<InjectIntl(FormattedText)
defaultMessage="{firstUser} "
id="mobile.combined_system_message.first_user"
values={
Object {
"firstUser": "@user1",
"secondUser": null,
}
}
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"fontSize": 14,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="added to the team"
id="mobile.combined_system_message.added_to_team"
/>
</Text>
<InjectIntl(FormattedText)
defaultMessage=" by {actor}."
id="mobile.combined_system_message.by_actor"
values={
Object {
"actor": "@user2",
}
}
/>
</Text>
`;
exports[`CombinedSystemMessage should match snapshot 2`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
>
<InjectIntl(FormattedText)
defaultMessage="{firstUser} and {secondUser} were "
id="combined_system_message.first_user_and_second_user_were"
values={
Object {
"firstUser": "first_user",
"secondUser": "second_user",
}
}
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"fontSize": 14,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="removed from the team"
id="combined_system_message.removed_from_team"
/>
</Text>
.
</Text>
`;

View file

@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LastUsers should match snapshot 1`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
>
<InjectIntl(FormattedText)
defaultMessage="E"
id="e"
values={
Object {
"lastUser": "User Two",
"users": "User One",
}
}
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"fontSize": 14,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="x"
id="x"
/>
</Text>
<InjectIntl(FormattedText)
defaultMessage="p"
id="p"
values={
Object {
"actor": "actor",
}
}
/>
</Text>
`;
exports[`LastUsers should match snapshot 2`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
>
<InjectIntl(FormattedText)
defaultMessage="{firstUser} and "
id="mobile.combined_system_message.first_user_and"
values={
Object {
"firstUser": "User One",
}
}
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
onPress={[Function]}
style={1}
>
<InjectIntl(FormattedText)
defaultMessage="{numOthers} others "
id="mobile.combined_system_message.others"
values={
Object {
"numOthers": 1,
}
}
/>
</Text>
<InjectIntl(FormattedText)
defaultMessage="were "
id="mobile.combined_system_message.were"
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"fontSize": 14,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="added to the team"
id="mobile.combined_system_message.added_to_team"
/>
</Text>
<InjectIntl(FormattedText)
defaultMessage=" by {actor}."
id="mobile.combined_system_message.by_actor"
values={
Object {
"actor": "actor",
}
}
/>
</Text>
`;

View file

@ -0,0 +1,350 @@
// 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 {Text} from 'react-native';
import {intlShape} from 'react-intl';
import {Posts} from 'mattermost-redux/constants';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
import LastUsers from './last_users';
const {
JOIN_CHANNEL, ADD_TO_CHANNEL, REMOVE_FROM_CHANNEL, LEAVE_CHANNEL,
JOIN_TEAM, ADD_TO_TEAM, REMOVE_FROM_TEAM, LEAVE_TEAM,
} = Posts.POST_TYPES;
const postTypeMessage = {
[JOIN_CHANNEL]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.joined_channel'],
defaultMessage: ['{firstUser} ', 'joined the channel'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.joined_channel'],
defaultMessage: ['{firstUser} and {secondUser} ', 'joined the channel'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.joined_channel'],
defaultMessage: ['{users} and {lastUser} ', 'joined the channel'],
},
},
[ADD_TO_CHANNEL]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.added_to_channel', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{firstUser} ', 'added to the channel', ' by {actor}.'],
},
one_you: {
id: ['mobile.combined_system_message.you_were', 'mobile.combined_system_message.added_to_channel', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['You were ', 'added to the channel', ' by {actor}.'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.added_to_channel', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{firstUser} and {secondUser} ', 'added to the channel', ' by {actor}.'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.added_to_channel', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{users} and {lastUser} ', 'added to the channel', ' by {actor}.'],
},
},
[REMOVE_FROM_CHANNEL]: {
one: {
id: ['mobile.combined_system_message.first_user_was', 'mobile.combined_system_message.removed_from_channel'],
defaultMessage: ['{firstUser} was ', 'removed from the channel'],
},
one_you: {
id: ['mobile.combined_system_message.you_were', 'mobile.combined_system_message.removed_from_channel'],
defaultMessage: ['You were ', 'removed from the channel'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user_were', 'mobile.combined_system_message.removed_from_channel'],
defaultMessage: ['{firstUser} and {secondUser} were ', 'removed from the channel'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user_were', 'mobile.combined_system_message.removed_from_channel'],
defaultMessage: ['{users} and {lastUser} were ', 'removed from the channel'],
},
},
[LEAVE_CHANNEL]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.left_channel'],
defaultMessage: ['{firstUser} ', 'left the channel'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.left_channel'],
defaultMessage: ['{firstUser} and {secondUser} ', 'left the channel'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.left_channel'],
defaultMessage: ['{users} and {lastUser} ', 'left the channel'],
},
},
[JOIN_TEAM]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.joined_team'],
defaultMessage: ['{firstUser} ', 'joined the team'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.joined_team'],
defaultMessage: ['{firstUser} and {secondUser} ', 'joined the team'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.joined_team'],
defaultMessage: ['{users} and {lastUser} ', 'joined the team'],
},
},
[ADD_TO_TEAM]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.added_to_team', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{firstUser} ', 'added to the team', ' by {actor}.'],
},
one_you: {
id: ['mobile.combined_system_message.you_were', 'mobile.combined_system_message.added_to_team', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['You were ', 'added to the team', ' by {actor}.'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.added_to_team', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{firstUser} and {secondUser} ', 'added to the team', ' by {actor}.'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.added_to_team', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['{users} and {lastUser} ', 'added to the team', ' by {actor}.'],
},
},
[REMOVE_FROM_TEAM]: {
one: {
id: ['mobile.combined_system_message.first_user_was', 'mobile.combined_system_message.removed_from_team'],
defaultMessage: ['{firstUser} was ', 'removed from the team'],
},
one_you: {
id: ['mobile.combined_system_message.you_were', 'mobile.combined_system_message.removed_from_team'],
defaultMessage: ['You were ', 'removed from the team'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user_were', 'mobile.combined_system_message.removed_from_team'],
defaultMessage: ['{firstUser} and {secondUser} were ', 'removed from the team'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user_were', 'mobile.combined_system_message.removed_from_team'],
defaultMessage: ['{users} and {lastUser} were ', 'removed from the team'],
},
},
[LEAVE_TEAM]: {
one: {
id: ['mobile.combined_system_message.first_user', 'mobile.combined_system_message.left_team'],
defaultMessage: ['{firstUser} ', 'left the team'],
},
two: {
id: ['mobile.combined_system_message.first_user_and_second_user', 'mobile.combined_system_message.left_team'],
defaultMessage: ['{firstUser} and {secondUser} ', 'left the team'],
},
many_expanded: {
id: ['mobile.combined_system_message.users_and_last_user', 'mobile.combined_system_message.left_team'],
defaultMessage: ['{users} and {lastUser} ', 'left the team'],
},
},
};
export default class CombinedSystemMessage extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
getProfilesByIds: PropTypes.func.isRequired,
}).isRequired,
allUserIds: PropTypes.array.isRequired,
currentUserId: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style,
messageData: PropTypes.array.isRequired,
teammateNameDisplay: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
this.state = {
userProfiles: [],
};
}
componentDidMount() {
this.loadUserProfiles();
}
loadUserProfiles = async () => {
const {
actions,
allUserIds,
} = this.props;
const {data: userProfiles} = await actions.getProfilesByIds(allUserIds);
this.setState({userProfiles});
}
getAllUsersDisplayName = () => {
const {userProfiles} = this.state;
const {
allUserIds,
currentUserId,
teammateNameDisplay,
} = this.props;
const {formatMessage} = this.context.intl;
const usersDisplayName = userProfiles.reduce((acc, user) => {
acc[user.id] = displayUsername(user, teammateNameDisplay, true);
return acc;
}, {});
const includesCurrentUser = allUserIds.includes(currentUserId);
if (includesCurrentUser) {
usersDisplayName[currentUserId] = formatMessage({id: 'mobile.combined_system_message.you', defaultMessage: 'You'});
}
return usersDisplayName;
}
getDisplayNameByIds = (userIds = []) => {
const {currentUserId} = this.props;
const usersDisplayName = this.getAllUsersDisplayName();
const displayNames = userIds.
filter((userId) => {
return userId !== currentUserId;
}).
map((userId) => {
return usersDisplayName[userId];
});
const includesCurrentUser = userIds.includes(currentUserId);
if (includesCurrentUser) {
displayNames.unshift(usersDisplayName[currentUserId]);
}
return displayNames;
}
renderSystemMessage(postType, userIds, actorId, style) {
const usersDisplayName = this.getDisplayNameByIds(userIds);
let actorDisplayName = actorId ? this.getDisplayNameByIds([actorId])[0] : '';
if (actorDisplayName && actorId === this.props.currentUserId) {
actorDisplayName = actorDisplayName.toLowerCase();
}
const firstUser = usersDisplayName[0];
const numOthers = usersDisplayName.length - 1;
let formattedMessage;
if (numOthers === 0) {
formattedMessage = this.renderFormattedMessage(
postTypeMessage[postType].one,
firstUser,
null,
actorDisplayName,
style,
);
if (
userIds[0] === this.props.currentUserId &&
postTypeMessage[postType].one_you
) {
formattedMessage = this.renderFormattedMessage(
postTypeMessage[postType].one_you,
null,
null,
actorDisplayName,
style,
);
}
} else if (numOthers === 1) {
formattedMessage = this.renderFormattedMessage(
postTypeMessage[postType].two,
firstUser,
usersDisplayName[1],
actorDisplayName,
style,
);
} else {
formattedMessage = (
<LastUsers
actor={actorDisplayName}
expandedLocale={postTypeMessage[postType].many_expanded}
postType={postType}
style={style}
userDisplayNames={usersDisplayName}
/>
);
}
return formattedMessage;
}
renderFormattedMessage = (localeFormat, firstUser, secondUser, actor, style) => {
return (
<Text>
<FormattedText
id={localeFormat.id[0]}
defaultMessage={localeFormat.defaultMessage[0]}
values={{
firstUser,
secondUser,
}}
/>
<Text style={style.activityType}>
<FormattedText
id={localeFormat.id[1]}
defaultMessage={localeFormat.defaultMessage[1]}
/>
</Text>
{localeFormat.id[2] ? (
<FormattedText
id={localeFormat.id[2]}
defaultMessage={localeFormat.defaultMessage[2]}
values={{actor}}
/>
) : ('.')
}
</Text>
);
}
render() {
const {
linkStyle,
messageData,
theme,
} = this.props;
const style = getStyleSheet(theme);
return (
<React.Fragment>
{messageData.map(({postType, userIds, actorId}) => {
return (
<React.Fragment key={postType + actorId}>
{this.renderSystemMessage(postType, userIds, actorId, {activityType: style.activityType, link: linkStyle})}
</React.Fragment>
);
})}
</React.Fragment>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
activityType: {
color: theme.centerChannelColor,
fontSize: 14,
fontWeight: 'bold',
},
};
});

View file

@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import {shallowWithIntl} from 'test/intl-test-helper';
import {Posts} from 'mattermost-redux/constants';
import CombinedSystemMessage from './combined_system_message';
describe('CombinedSystemMessage', () => {
const baseProps = {
actions: {
getProfilesByIds: () => {}, // eslint-disable-line no-empty-function
},
allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'],
currentUserId: 'user_id_3',
linkStyle: 1,
messageData: [
{postType: Posts.POST_TYPES.ADD_TO_TEAM, userIds: ['user_id_1'], actorId: 'user_id_2'},
],
teammateNameDisplay: 'username',
theme: {centerChannelColor: '#aaa'},
};
test('should match snapshot', () => {
const props = {
...baseProps,
actions: {getProfilesByIds: jest.fn(() => Promise.resolve({data: true}))},
};
const wrapper = shallowWithIntl(
<CombinedSystemMessage {...props}/>
);
wrapper.setState({userProfiles: [{id: 'user_id_1', username: 'user1'}, {id: 'user_id_2', username: 'user2'}, {id: 'user_id_3', username: 'user3'}]});
const {postType, userIds, actorId} = baseProps.messageData[0];
expect(wrapper.instance().renderSystemMessage(postType, userIds, actorId, {activityType: {fontSize: 14}}, 1)).toMatchSnapshot();
// on componentDidMount
expect(props.actions.getProfilesByIds).toHaveBeenCalledTimes(1);
expect(props.actions.getProfilesByIds).toHaveBeenCalledWith(props.allUserIds);
});
test('should match snapshot', () => {
const props = {
...baseProps,
actions: {getProfilesByIds: jest.fn(() => Promise.resolve({data: true}))},
};
const localeFormat = {
id: ['combined_system_message.first_user_and_second_user_were', 'combined_system_message.removed_from_team'],
defaultMessage: ['{firstUser} and {secondUser} were ', 'removed from the team'],
};
const wrapper = shallowWithIntl(
<CombinedSystemMessage {...props}/>
);
wrapper.setState({userProfiles: [{id: 'user_id_1', username: 'user1'}, {id: 'user_id_2', username: 'user2'}, {id: 'user_id_3', username: 'user3'}]});
expect(wrapper.instance().renderFormattedMessage(localeFormat, 'first_user', 'second_user', 'actor', {activityType: {fontSize: 14}})).toMatchSnapshot();
});
});

View file

@ -0,0 +1,29 @@
// 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 {getProfilesByIds} from 'mattermost-redux/actions/users';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import CombinedSystemMessage from './combined_system_message';
function mapStateToProps(state) {
return {
currentUserId: getCurrentUserId(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfilesByIds,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CombinedSystemMessage);

View file

@ -0,0 +1,159 @@
// 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 {Text} from 'react-native';
import {Posts} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
const typeMessage = {
[Posts.POST_TYPES.ADD_TO_CHANNEL]: {
id: ['mobile.combined_system_message.were', 'mobile.combined_system_message.added_to_channel', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['were ', 'added to the channel', ' by {actor}.'],
},
[Posts.POST_TYPES.JOIN_CHANNEL]: {
id: ['', 'mobile.combined_system_message.joined_channel'],
defaultMessage: ['', 'joined the channel'],
},
[Posts.POST_TYPES.LEAVE_CHANNEL]: {
id: ['', 'mobile.combined_system_message.left_channel', ''],
defaultMessage: ['', 'left the channel'],
},
[Posts.POST_TYPES.REMOVE_FROM_CHANNEL]: {
id: ['mobile.combined_system_message.were', 'mobile.combined_system_message.removed_from_channel'],
defaultMessage: ['were ', 'removed from the channel'],
},
[Posts.POST_TYPES.ADD_TO_TEAM]: {
id: ['mobile.combined_system_message.were', 'mobile.combined_system_message.added_to_team', 'mobile.combined_system_message.by_actor'],
defaultMessage: ['were ', 'added to the team', ' by {actor}.'],
},
[Posts.POST_TYPES.JOIN_TEAM]: {
id: ['', 'mobile.combined_system_message.joined_team'],
defaultMessage: ['', 'joined the team'],
},
[Posts.POST_TYPES.LEAVE_TEAM]: {
id: ['', 'mobile.combined_system_message.left_team'],
defaultMessage: ['', 'left the team'],
},
[Posts.POST_TYPES.REMOVE_FROM_TEAM]: {
id: ['', 'mobile.combined_system_message.removed_from_team'],
defaultMessage: ['were ', 'removed from the team'],
},
};
export default class LastUsers extends React.PureComponent {
static propTypes = {
actor: PropTypes.string,
expandedLocale: PropTypes.object.isRequired,
postType: PropTypes.string.isRequired,
style: PropTypes.object.isRequired,
userDisplayNames: PropTypes.array.isRequired,
};
constructor(props) {
super(props);
this.state = {
expand: false,
};
}
handleOnClick = (e) => {
e.preventDefault();
this.setState({expand: true});
}
renderExpandedView = (expandedLocale, userDisplayNames, actor, lastIndex, style) => {
return (
<Text>
<FormattedText
id={expandedLocale.id[0]}
defaultMessage={expandedLocale.defaultMessage[0]}
values={{
users: userDisplayNames.slice(0, lastIndex).join(', '),
lastUser: userDisplayNames[lastIndex],
}}
/>
<Text style={style.activityType}>
<FormattedText
id={expandedLocale.id[1]}
defaultMessage={expandedLocale.defaultMessage[1]}
/>
</Text>
{expandedLocale.id[2] ? (
<FormattedText
id={expandedLocale.id[2]}
defaultMessage={expandedLocale.defaultMessage[2]}
values={{actor}}
/>
) : ('.')
}
</Text>
);
}
renderCollapsedView = (postType, userDisplayNames, actor, lastIndex, style) => {
return (
<Text>
<FormattedText
id={'mobile.combined_system_message.first_user_and'}
defaultMessage={'{firstUser} and '}
values={{firstUser: userDisplayNames[0]}}
/>
<Text
style={style.link}
onPress={this.handleOnClick}
>
<FormattedText
id={'mobile.combined_system_message.others'}
defaultMessage={'{numOthers} others '}
values={{numOthers: lastIndex}}
/>
</Text>
{typeMessage[postType].id[0] &&
<FormattedText
id={typeMessage[postType].id[0]}
defaultMessage={typeMessage[postType].defaultMessage[0]}
/>
}
<Text style={style.activityType}>
<FormattedText
id={typeMessage[postType].id[1]}
defaultMessage={typeMessage[postType].defaultMessage[1]}
/>
</Text>
{typeMessage[postType].id[2] ? (
<FormattedText
id={typeMessage[postType].id[2]}
defaultMessage={typeMessage[postType].defaultMessage[2]}
values={{actor}}
/>
) : ('.')
}
</Text>
);
}
render() {
const {expand} = this.state;
const {
actor,
expandedLocale,
postType,
userDisplayNames,
style,
} = this.props;
const lastIndex = userDisplayNames.length - 1;
if (expand) {
return this.renderExpandedView(expandedLocale, userDisplayNames, actor, lastIndex, style);
}
return this.renderCollapsedView(postType, userDisplayNames, actor, lastIndex, style);
}
}

View file

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import {Posts} from 'mattermost-redux/constants';
import {shallowWithIntl} from 'test/intl-test-helper';
import LastUsers from './last_users';
describe('LastUsers', () => {
const baseProps = {
actor: 'actor',
expandedLocale: {id: 'expanded_locale_id', defaultMessage: 'Expanded Locale'},
postType: Posts.POST_TYPES.ADD_TO_TEAM,
style: {activityType: {fontSize: 14}, link: 1},
userDisplayNames: ['User One', 'User Two'],
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(
<LastUsers {...baseProps}/>
);
const expanded = wrapper.instance().renderExpandedView(baseProps.expandedLocale, baseProps.userDisplayNames, baseProps.actor, 1, baseProps.style);
expect(expanded).toMatchSnapshot();
const collapsed = wrapper.instance().renderCollapsedView(baseProps.postType, baseProps.userDisplayNames, baseProps.actor, 1, baseProps.style);
expect(collapsed).toMatchSnapshot();
});
test('should match state on handleOnClick', () => {
const wrapper = shallowWithIntl(
<LastUsers {...baseProps}/>
);
wrapper.setState({expand: false});
wrapper.instance().handleOnClick({preventDefault: jest.fn()});
expect(wrapper.state('expand')).toEqual(true);
});
});

View file

@ -66,6 +66,7 @@ function mapStateToProps(state, ownProps) {
return {
postProps: post.props || {},
postType: post.type || '',
fileIds: post.file_ids,
hasBeenDeleted: post.state === Posts.POST_DELETED,
hasBeenEdited: isEdited(post),

View file

@ -15,6 +15,9 @@ import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/Ionicons';
import LinearGradient from 'react-native-linear-gradient';
import {Posts} from 'mattermost-redux/constants';
import CombinedSystemMessage from 'app/components/combined_system_message';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import OptionsContext from 'app/components/options_context';
@ -65,6 +68,7 @@ export default class PostBody extends PureComponent {
onPress: PropTypes.func,
postId: PropTypes.string.isRequired,
postProps: PropTypes.object,
postType: PropTypes.string,
renderReplyBar: PropTypes.func,
showAddReaction: PropTypes.bool,
showLongPost: PropTypes.bool.isRequired,
@ -401,6 +405,8 @@ export default class PostBody extends PureComponent {
onFailedPostPress,
onPermalinkPress,
onPress,
postProps,
postType,
renderReplyBar,
theme,
toggleSelected,
@ -434,6 +440,20 @@ export default class PostBody extends PureComponent {
body = (<View>{messageComponent}</View>);
} else if (isPostAddChannelMember) {
messageComponent = this.renderAddChannelMember(style, textStyles);
} else if (postType === Posts.POST_TYPES.COMBINED_USER_ACTIVITY) {
const {allUserIds, messageData} = postProps.user_activity;
messageComponent = (
<View style={style.row}>
<View style={style.flex}>
<CombinedSystemMessage
allUserIds={allUserIds}
linkStyle={textStyles.link}
messageData={messageData}
theme={theme}
/>
</View>
</View>
);
} else if (message.length) {
messageComponent = (
<View style={style.row}>

View file

@ -2125,6 +2125,26 @@
"mobile.client_upgrade.no_upgrade_subtitle": "You already have the latest version.",
"mobile.client_upgrade.no_upgrade_title": "Your App Is Up to Date",
"mobile.client_upgrade.upgrade": "Update",
"mobile.combined_system_message.first_user": "{firstUser} ",
"mobile.combined_system_message.first_user_was": "{firstUser} was ",
"mobile.combined_system_message.first_user_and_second_user": "{firstUser} and {secondUser} ",
"mobile.combined_system_message.first_user_and_second_user_were": "{firstUser} and {secondUser} were ",
"mobile.combined_system_message.users_and_last_user": "{users} and {lastUser} ",
"mobile.combined_system_message.users_and_last_user_were": "{users} and {lastUser} were ",
"mobile.combined_system_message.by_actor": " by {actor}.",
"mobile.combined_system_message.you": "You",
"mobile.combined_system_message.you_were": "You were ",
"mobile.combined_system_message.were": "were ",
"mobile.combined_system_message.first_user_and": "{firstUser} and ",
"mobile.combined_system_message.others": "{numOthers} others ",
"mobile.combined_system_message.joined_channel": "joined the channel",
"mobile.combined_system_message.added_to_channel": "added to the channel",
"mobile.combined_system_message.removed_from_channel": "removed from the channel",
"mobile.combined_system_message.left_channel": "left the channel",
"mobile.combined_system_message.joined_team": "joined the team",
"mobile.combined_system_message.added_to_team": "added to the team",
"mobile.combined_system_message.removed_from_team": "removed from the team",
"mobile.combined_system_message.left_team": "left the team",
"mobile.commands.error_title": "Error Executing Command",
"mobile.components.channels_list_view.yourChannels": "Your channels:",
"mobile.components.error_list.dismiss_all": "Dismiss All",

22
test/intl-test-helper.js Normal file
View file

@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {IntlProvider, intlShape} from 'react-intl';
import {mount, shallow} from 'enzyme';
const intlProvider = new IntlProvider({locale: 'en'}, {});
const {intl} = intlProvider.getChildContext();
export function shallowWithIntl(node, {context} = {}) {
return shallow(React.cloneElement(node, {intl}), {
context: Object.assign({}, context, {intl}),
});
}
export function mountWithIntl(node, {context, childContextTypes} = {}) {
return mount(React.cloneElement(node, {intl}), {
context: Object.assign({}, context, {intl}),
childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes),
});
}