diff --git a/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap new file mode 100644 index 000000000..e864ae21a --- /dev/null +++ b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] = ` + +`; + +exports[`renderSystemMessage uses renderer for Channel Header update 1`] = ` + +`; + +exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = ` + +`; + +exports[`renderSystemMessage uses renderer for archived channel 1`] = ` + +`; diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index b891ee324..b49a69f92 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -13,6 +13,7 @@ import Icon from 'react-native-vector-icons/Ionicons'; import {Posts} from 'mattermost-redux/constants'; import CombinedSystemMessage from 'app/components/combined_system_message'; +import {renderSystemMessage} from './system_message_helpers'; import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; import MarkdownEmoji from 'app/components/markdown/markdown_emoji'; @@ -348,6 +349,13 @@ export default class PostBody extends PureComponent { const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message; const isPendingOrFailedPost = isPending || isFailed; + const messageStyles = {messageStyle, textStyles}; + const intl = this.context.intl; + const systemMessage = renderSystemMessage(this.props, messageStyles, intl); + if (systemMessage) { + return systemMessage; + } + let body; let messageComponent; if (hasBeenDeleted) { diff --git a/app/components/post_body/post_body.test.js b/app/components/post_body/post_body.test.js index 3c299f9ec..c8cb6d92a 100644 --- a/app/components/post_body/post_body.test.js +++ b/app/components/post_body/post_body.test.js @@ -9,6 +9,9 @@ import PostBodyAdditionalContent from 'app/components/post_body_additional_conte import {shallowWithIntl} from 'test/intl-test-helper'; import PostBody from './post_body.js'; +import * as SystemMessageHelpers from './system_message_helpers'; + +jest.mock('./system_message_helpers'); describe('PostBody', () => { const baseProps = { @@ -122,4 +125,9 @@ describe('PostBody', () => { instance.measurePost(event); expect(wrapper.state('isLongPost')).toEqual(false); }); + + test('should return system message as post body', () => { + shallowWithIntl(); + expect(SystemMessageHelpers.renderSystemMessage.mock.calls.length).toBe(1); + }); }); diff --git a/app/components/post_body/system_message_helpers.js b/app/components/post_body/system_message_helpers.js new file mode 100644 index 000000000..2a9c2f569 --- /dev/null +++ b/app/components/post_body/system_message_helpers.js @@ -0,0 +1,160 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Posts} from 'mattermost-redux/constants'; +import Markdown from 'app/components/markdown'; +import {t} from 'app/utils/i18n'; + +const renderUsername = (value) => { + return (value[0] === '@') ? value : `@${value}`; +}; + +const renderMessage = (postBodyProps, styles, intl, localeHolder, values) => { + const {onPress} = postBodyProps; + const {messageStyle, textStyles} = styles; + + return ( + + ); +}; + +const renderHeaderChangeMessage = (postBodyProps, styles, intl) => { + const {postProps} = postBodyProps; + let values; + + if (!postProps.username) { + return null; + } + + const username = renderUsername(postProps.username); + const oldHeader = postProps.old_header; + const newHeader = postProps.new_header; + let localeHolder; + + if (postProps.new_header) { + if (postProps.old_header) { + localeHolder = { + id: t('mobile.system_message.update_channel_header_message_and_forget.updated_from'), + defaultMessage: '{username} updated the channel header from: {oldHeader} to: {newHeader}', + }; + + values = {username, oldHeader, newHeader}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } + + localeHolder = { + id: t('mobile.system_message.update_channel_header_message_and_forget.updated_to'), + defaultMessage: '{username} updated the channel header to: {newHeader}', + }; + + values = {username, oldHeader, newHeader}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } else if (postProps.old_header) { + localeHolder = { + id: t('mobile.system_message.update_channel_header_message_and_forget.removed'), + defaultMessage: '{username} removed the channel header (was: {oldHeader})', + }; + + values = {username, oldHeader, newHeader}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } + + return null; +}; + +const renderPurposeChangeMessage = (postBodyProps, styles, intl) => { + const {postProps} = postBodyProps; + let values; + + if (!postProps.username) { + return null; + } + + const username = renderUsername(postProps.username); + const oldPurpose = postProps.old_purpose; + const newPurpose = postProps.new_purpose; + let localeHolder; + + if (postProps.new_purpose) { + if (postProps.old_purpose) { + localeHolder = { + id: t('mobile.system_message.update_channel_purpose_message.updated_from'), + defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}', + }; + + values = {username, oldPurpose, newPurpose}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } + + localeHolder = { + id: t('mobile.system_message.update_channel_purpose_message.updated_to'), + defaultMessage: '{username} updated the channel purpose to: {newPurpose}', + }; + + values = {username, oldPurpose, newPurpose}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } else if (postProps.old_purpose) { + localeHolder = { + id: t('mobile.system_message.update_channel_purpose_message.removed'), + defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})', + }; + + values = {username, oldPurpose, newPurpose}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); + } + + return null; +}; + +const renderDisplayNameChangeMessage = (postBodyProps, styles, intl) => { + const {postProps} = postBodyProps; + const oldDisplayName = postProps.old_displayname; + const newDisplayName = postProps.new_displayname; + + if (!(postProps.username && postProps.old_displayname && postProps.new_displayname)) { + return null; + } + + const username = renderUsername(postProps.username); + const localeHolder = { + id: t('mobile.system_message.update_channel_displayname_message_and_forget.updated_from'), + defaultMessage: '{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}', + }; + + const values = {username, oldDisplayName, newDisplayName}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); +}; + +const renderArchivedMessage = (postBodyProps, styles, intl) => { + const {postProps} = postBodyProps; + + const username = renderUsername(postProps.username); + const localeHolder = { + id: t('mobile.system_message.channel_archived_message'), + defaultMessage: '{username} archived the channel', + }; + + const values = {username}; + return renderMessage(postBodyProps, styles, intl, localeHolder, values); +}; + +const systemMessageRenderers = { + [Posts.POST_TYPES.HEADER_CHANGE]: renderHeaderChangeMessage, + [Posts.POST_TYPES.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage, + [Posts.POST_TYPES.PURPOSE_CHANGE]: renderPurposeChangeMessage, + [Posts.POST_TYPES.CHANNEL_DELETED]: renderArchivedMessage, +}; + +export const renderSystemMessage = (postBodyProps, styles, intl) => { + const renderer = systemMessageRenderers[postBodyProps.postType]; + if (!renderer) { + return null; + } + return renderer(postBodyProps, styles, intl); +}; diff --git a/app/components/post_body/system_message_helpers.test.js b/app/components/post_body/system_message_helpers.test.js new file mode 100644 index 000000000..e533edda0 --- /dev/null +++ b/app/components/post_body/system_message_helpers.test.js @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as SystemMessageHelpers from './system_message_helpers'; +import {Posts} from 'mattermost-redux/constants'; + +const basePostBodyProps = { + postProps: { + username: 'username', + }, + onPress: jest.fn(), +}; + +const mockStyles = { + messageStyle: {}, + textStyles: {}, +}; + +const mockIntl = { + formatMessage: ({defaultMessage}) => defaultMessage, +}; + +describe('renderSystemMessage', () => { + test('uses renderer for Channel Header update', () => { + const postBodyProps = { + ...basePostBodyProps, + postProps: { + ...basePostBodyProps.postProps, + old_header: 'old header', + new_header: 'new header', + }, + postType: Posts.POST_TYPES.HEADER_CHANGE, + }; + const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl); + expect(renderedMessage).toMatchSnapshot(); + }); + + test('uses renderer for Channel Display Name update', () => { + const postBodyProps = { + ...basePostBodyProps, + postProps: { + ...basePostBodyProps.postProps, + old_displayname: 'old displayname', + new_displayname: 'new displayname', + }, + postType: Posts.POST_TYPES.DISPLAYNAME_CHANGE, + }; + + const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl); + expect(renderedMessage).toMatchSnapshot(); + }); + + test('uses renderer for Channel Purpose update', () => { + const postBodyProps = { + ...basePostBodyProps, + postProps: { + ...basePostBodyProps.postProps, + old_purpose: 'old purpose', + new_purpose: 'new purpose', + }, + postType: Posts.POST_TYPES.PURPOSE_CHANGE, + }; + + const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl); + expect(renderedMessage).toMatchSnapshot(); + }); + + test('uses renderer for archived channel', () => { + const postBodyProps = { + ...basePostBodyProps, + postProps: { + ...basePostBodyProps.postProps, + }, + postType: Posts.POST_TYPES.CHANNEL_DELETED, + }; + + const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl); + expect(renderedMessage).toMatchSnapshot(); + }); + + test('is null for non-qualifying system messages', () => { + const postBodyProps = { + ...basePostBodyProps, + postType: 'not_relevant', + }; + + const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl); + expect(renderedMessage).toBeNull(); + }); +}); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index bf85fdfef..061d6bc87 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -463,6 +463,14 @@ "mobile.storage_permission_denied_description": "Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.", "mobile.storage_permission_denied_title": "{applicationName} would like to access your files", "mobile.suggestion.members": "Members", + "mobile.system_message.channel_archived_message": "{username} archived the channel", + "mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}", + "mobile.system_message.update_channel_header_message_and_forget.removed": "{username} removed the channel header (was: {oldHeader})", + "mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} updated the channel header from: {oldHeader} to: {newHeader}", + "mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} updated the channel header to: {newHeader}", + "mobile.system_message.update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {oldPurpose})", + "mobile.system_message.update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}", + "mobile.system_message.update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {newPurpose}", "mobile.terms_of_service.alert_cancel": "Cancel", "mobile.terms_of_service.alert_ok": "OK", "mobile.terms_of_service.alert_retry": "Try Again",