diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/at_mention/at_mention.jsx | 79 | ||||
-rw-r--r-- | webapp/components/at_mention/index.jsx | 27 | ||||
-rw-r--r-- | webapp/components/post_view/post_body/post_body.jsx | 25 | ||||
-rw-r--r-- | webapp/components/post_view/post_message_view/post_message_view.jsx | 54 |
4 files changed, 162 insertions, 23 deletions
diff --git a/webapp/components/at_mention/at_mention.jsx b/webapp/components/at_mention/at_mention.jsx new file mode 100644 index 000000000..760884b88 --- /dev/null +++ b/webapp/components/at_mention/at_mention.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class AtMention extends React.PureComponent { + static propTypes = { + mentionName: PropTypes.string.isRequired, + usersByUsername: PropTypes.object.isRequired, + actions: PropTypes.shape({ + searchForTerm: PropTypes.func.isRequired + }).isRequired + }; + + constructor(props) { + super(props); + + this.state = { + username: this.getUsernameFromMentionName(props) + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.mentionName !== this.props.mentionName || nextProps.usersByUsername !== this.props.usersByUsername) { + this.setState({ + username: this.getUsernameFromMentionName(nextProps) + }); + } + } + + getUsernameFromMentionName(props) { + let mentionName = props.mentionName; + + while (mentionName.length > 0) { + if (props.usersByUsername[mentionName]) { + return props.usersByUsername[mentionName].username; + } + + // Repeatedly trim off trailing punctuation in case this is at the end of a sentence + if ((/[._-]$/).test(mentionName)) { + mentionName = mentionName.substring(0, mentionName.length - 1); + } else { + break; + } + } + + return ''; + } + + search = (e) => { + e.preventDefault(); + + this.props.actions.searchForTerm(this.state.username); + } + + render() { + const username = this.state.username; + + if (!username) { + return <span>{'@' + this.props.mentionName}</span>; + } + + const suffix = this.props.mentionName.substring(username.length); + + return ( + <span> + <a + className='mention-link' + href='#' + onClick={this.search} + > + {'@' + username} + </a> + {suffix} + </span> + ); + } +} diff --git a/webapp/components/at_mention/index.jsx b/webapp/components/at_mention/index.jsx new file mode 100644 index 000000000..c733158c6 --- /dev/null +++ b/webapp/components/at_mention/index.jsx @@ -0,0 +1,27 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; + +import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users'; + +import {searchForTerm} from 'actions/post_actions.jsx'; + +import AtMention from './at_mention.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + usersByUsername: getUsersByUsername(state) + }; +} + +function mapDispatchToProps() { + return { + actions: { + searchForTerm + } + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AtMention); diff --git a/webapp/components/post_view/post_body/post_body.jsx b/webapp/components/post_view/post_body/post_body.jsx index a14141dcd..044b46c55 100644 --- a/webapp/components/post_view/post_body/post_body.jsx +++ b/webapp/components/post_view/post_body/post_body.jsx @@ -1,20 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from 'utils/utils.jsx'; -import * as PostUtils from 'utils/post_utils.jsx'; -import {Posts} from 'mattermost-redux/constants'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import * as PostActions from 'actions/post_actions.jsx'; -import CommentedOnFilesMessage from 'components/post_view/commented_on_files_message'; import FileAttachmentListContainer from 'components/file_attachment_list'; +import CommentedOnFilesMessage from 'components/post_view/commented_on_files_message'; import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx'; -import PostMessageContainer from 'components/post_view/post_message_view'; -import ReactionListContainer from 'components/post_view/reaction_list'; import FailedPostOptions from 'components/post_view/failed_post_options'; +import PostMessageView from 'components/post_view/post_message_view'; +import ReactionListContainer from 'components/post_view/reaction_list'; -import React from 'react'; -import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; +import * as Utils from 'utils/utils.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; + +import {Posts} from 'mattermost-redux/constants'; export default class PostBody extends React.PureComponent { static propTypes = { @@ -89,7 +92,7 @@ export default class PostBody extends React.PureComponent { name = ( <a className='theme' - onClick={Utils.searchForTerm.bind(null, username)} + onClick={PostActions.searchForTerm.bind(null, username)} > {username} </a> @@ -156,7 +159,7 @@ export default class PostBody extends React.PureComponent { className={postClass} > {failedOptions} - <PostMessageContainer + <PostMessageView lastPostCount={this.props.lastPostCount} post={this.props.post} /> diff --git a/webapp/components/post_view/post_message_view/post_message_view.jsx b/webapp/components/post_view/post_message_view/post_message_view.jsx index 66a8d01f8..d066183ff 100644 --- a/webapp/components/post_view/post_message_view/post_message_view.jsx +++ b/webapp/components/post_view/post_message_view/post_message_view.jsx @@ -1,17 +1,21 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import {Parser, ProcessNodeDefinitions} from 'html-to-react'; import PropTypes from 'prop-types'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; +import AtMention from 'components/at_mention'; + +import store from 'stores/redux_store.jsx'; + import * as PostUtils from 'utils/post_utils.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels'; import {Posts} from 'mattermost-redux/constants'; -import store from 'stores/redux_store.jsx'; import {renderSystemMessage} from './system_message_helpers.jsx'; @@ -44,11 +48,6 @@ export default class PostMessageView extends React.PureComponent { mentionKeys: PropTypes.arrayOf(PropTypes.string), /* - * Object mapping usernames to users - */ - usernameMap: PropTypes.object, - - /* * The URL that the app is hosted on */ siteUrl: PropTypes.string, @@ -66,8 +65,7 @@ export default class PostMessageView extends React.PureComponent { static defaultProps = { options: {}, - mentionKeys: [], - usernameMap: {} + mentionKeys: [] }; renderDeletedPost() { @@ -96,6 +94,34 @@ export default class PostMessageView extends React.PureComponent { ); } + postMessageHtmlToComponent(html) { + const parser = new Parser(); + const attrib = 'data-mention'; + const processNodeDefinitions = new ProcessNodeDefinitions(React); + + function isValidNode() { + return true; + } + + const processingInstructions = [ + { + replaceChildren: true, + shouldProcessNode: (node) => node.attribs && node.attribs[attrib], + processNode: (node) => { + const mentionName = node.attribs[attrib]; + + return <AtMention mentionName={mentionName}/>; + } + }, + { + shouldProcessNode: () => true, + processNode: processNodeDefinitions.processDefaultNode + } + ]; + + return parser.parseWithInstructions(html, isValidNode, processingInstructions); + } + render() { if (this.props.post.state === Posts.POST_DELETED) { return this.renderDeletedPost(); @@ -109,7 +135,7 @@ export default class PostMessageView extends React.PureComponent { emojis: this.props.emojis, siteURL: this.props.siteUrl, mentionKeys: this.props.mentionKeys, - usernameMap: this.props.usernameMap, + atMentions: true, channelNamesMap: getChannelsNameMapInCurrentTeam(store.getState()), team: this.props.team }); @@ -124,14 +150,18 @@ export default class PostMessageView extends React.PureComponent { postId = Utils.createSafeId('lastPostMessageText' + this.props.lastPostCount); } + const htmlFormattedText = TextFormatting.formatText(this.props.post.message, options); + const postMessageComponent = this.postMessageHtmlToComponent(htmlFormattedText); + return ( <div> <span id={postId} className='post-message__text' onClick={Utils.handleFormattedTextClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}} - /> + > + {postMessageComponent} + </span> {this.renderEditedIndicator()} </div> ); |