diff --git a/app/components/combined_system_message/__snapshots__/combined_system_message.test.js.snap b/app/components/combined_system_message/__snapshots__/combined_system_message.test.js.snap
new file mode 100644
index 000000000..36f4f854a
--- /dev/null
+++ b/app/components/combined_system_message/__snapshots__/combined_system_message.test.js.snap
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CombinedSystemMessage should match snapshot 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`CombinedSystemMessage should match snapshot 2`] = `
+
+
+
+
+
+ .
+
+`;
diff --git a/app/components/combined_system_message/__snapshots__/last_users.test.js.snap b/app/components/combined_system_message/__snapshots__/last_users.test.js.snap
new file mode 100644
index 000000000..562d56232
--- /dev/null
+++ b/app/components/combined_system_message/__snapshots__/last_users.test.js.snap
@@ -0,0 +1,107 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LastUsers should match snapshot 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`LastUsers should match snapshot 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/combined_system_message/combined_system_message.js b/app/components/combined_system_message/combined_system_message.js
new file mode 100644
index 000000000..a7be1859c
--- /dev/null
+++ b/app/components/combined_system_message/combined_system_message.js
@@ -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 = (
+
+ );
+ }
+
+ return formattedMessage;
+ }
+
+ renderFormattedMessage = (localeFormat, firstUser, secondUser, actor, style) => {
+ return (
+
+
+
+
+
+ {localeFormat.id[2] ? (
+
+ ) : ('.')
+ }
+
+ );
+ }
+
+ render() {
+ const {
+ linkStyle,
+ messageData,
+ theme,
+ } = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+ {messageData.map(({postType, userIds, actorId}) => {
+ return (
+
+ {this.renderSystemMessage(postType, userIds, actorId, {activityType: style.activityType, link: linkStyle})}
+
+ );
+ })}
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ activityType: {
+ color: theme.centerChannelColor,
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+ };
+});
diff --git a/app/components/combined_system_message/combined_system_message.test.js b/app/components/combined_system_message/combined_system_message.test.js
new file mode 100644
index 000000000..8b02855cd
--- /dev/null
+++ b/app/components/combined_system_message/combined_system_message.test.js
@@ -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(
+
+ );
+ 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(
+
+ );
+
+ 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();
+ });
+});
diff --git a/app/components/combined_system_message/index.js b/app/components/combined_system_message/index.js
new file mode 100644
index 000000000..873625e49
--- /dev/null
+++ b/app/components/combined_system_message/index.js
@@ -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);
diff --git a/app/components/combined_system_message/last_users.js b/app/components/combined_system_message/last_users.js
new file mode 100644
index 000000000..e22779d8c
--- /dev/null
+++ b/app/components/combined_system_message/last_users.js
@@ -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 (
+
+
+
+
+
+ {expandedLocale.id[2] ? (
+
+ ) : ('.')
+ }
+
+ );
+ }
+
+ renderCollapsedView = (postType, userDisplayNames, actor, lastIndex, style) => {
+ return (
+
+
+
+
+
+ {typeMessage[postType].id[0] &&
+
+ }
+
+
+
+ {typeMessage[postType].id[2] ? (
+
+ ) : ('.')
+ }
+
+ );
+ }
+
+ 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);
+ }
+}
diff --git a/app/components/combined_system_message/last_users.test.js b/app/components/combined_system_message/last_users.test.js
new file mode 100644
index 000000000..ed48ae71d
--- /dev/null
+++ b/app/components/combined_system_message/last_users.test.js
@@ -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(
+
+ );
+
+ 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(
+
+ );
+
+ wrapper.setState({expand: false});
+ wrapper.instance().handleOnClick({preventDefault: jest.fn()});
+ expect(wrapper.state('expand')).toEqual(true);
+ });
+});
diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js
index dead936e5..484d0d76e 100644
--- a/app/components/post_body/index.js
+++ b/app/components/post_body/index.js
@@ -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),
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index e60f51913..e953f1ee6 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -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 = ({messageComponent});
} else if (isPostAddChannelMember) {
messageComponent = this.renderAddChannelMember(style, textStyles);
+ } else if (postType === Posts.POST_TYPES.COMBINED_USER_ACTIVITY) {
+ const {allUserIds, messageData} = postProps.user_activity;
+ messageComponent = (
+
+
+
+
+
+ );
} else if (message.length) {
messageComponent = (
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 82a9d75e2..e5b021516 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/test/intl-test-helper.js b/test/intl-test-helper.js
new file mode 100644
index 000000000..cd575ba69
--- /dev/null
+++ b/test/intl-test-helper.js
@@ -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),
+ });
+}