diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2016-08-29 09:50:00 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-08-29 09:50:00 -0400 |
commit | 167dd22eefeeeb9c1eaebd990a4f5902bd366302 (patch) | |
tree | 6ddb15a80b2a608d42e20df72b98c0ae72821671 /webapp/components | |
parent | 55342e8fe16613f06528ed1aa726231e9b597d26 (diff) | |
download | chat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.tar.gz chat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.tar.bz2 chat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.zip |
PLT-1752/PLT-3567/PLT-3998 Highlighting links in search, unit tests for autolinking (#3865)
* Added highlighting to links when their URL includes the search term
* Decoupling UserStore from react-router to allow for unit tests involving it
* PLT-3998 Added SiteURL as an option to be passed into the text formatting code
* Removed reference to PreferenceStore and window from TextFormatting
* Refactored TextFormatting to remove remaining browser-only code
* Updated ChannelHeader and MessageWrapper to match the changes to TextFormatting
* Increased max listeners for Preference and Emoji stores
* PLT-3832 Added automated unit tests for autolinking
* PLT-3567 Rerender posts when mention keywords change
* Updated RHS and search to match the changes to TextFormatting
* Broke TextFormatting's dependency on the UserStore
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/channel_header.jsx | 4 | ||||
-rw-r--r-- | webapp/components/message_wrapper.jsx | 13 | ||||
-rw-r--r-- | webapp/components/post_view/components/post.jsx | 7 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_body.jsx | 14 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_list.jsx | 2 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_message_container.jsx | 87 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_message_view.jsx | 66 | ||||
-rw-r--r-- | webapp/components/post_view/post_view_controller.jsx | 16 | ||||
-rw-r--r-- | webapp/components/rhs_comment.jsx | 10 | ||||
-rw-r--r-- | webapp/components/rhs_root_post.jsx | 10 | ||||
-rw-r--r-- | webapp/components/search_results_item.jsx | 16 |
11 files changed, 181 insertions, 64 deletions
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 65c856b4a..6cecc04bd 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -581,9 +581,9 @@ export default class ChannelHeader extends React.Component { ref='headerOverlay' > <div - onClick={TextFormatting.handleClick} + onClick={Utils.handleFormattedTextClick} className='description' - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false, siteURL: Utils.getSiteURL()})}} /> </OverlayTrigger> </div> diff --git a/webapp/components/message_wrapper.jsx b/webapp/components/message_wrapper.jsx index 5e9939efa..4dba1024e 100644 --- a/webapp/components/message_wrapper.jsx +++ b/webapp/components/message_wrapper.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as TextFormatting from 'utils/text_formatting.jsx'; +import * as Utils from 'utils/utils.jsx'; import React from 'react'; @@ -10,9 +11,19 @@ export default class MessageWrapper extends React.Component { super(props); this.state = {}; } + render() { if (this.props.message) { - return <div dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, this.props.options)}}/>; + const options = Object.assign({}, this.props.options, { + siteURL: Utils.getSiteURL() + }); + + return ( + <div + onClick={Utils.handleFormattedTextClick} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}} + /> + ); } return <div/>; diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 1b94f717d..e9019bf38 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -114,10 +114,6 @@ export default class Post extends React.Component { return true; } - if (nextProps.emojis !== this.props.emojis) { - return true; - } - if (nextState.dropdownOpened !== this.state.dropdownOpened) { return true; } @@ -259,7 +255,6 @@ export default class Post extends React.Component { handleCommentClick={this.handleCommentClick} compactDisplay={this.props.compactDisplay} previewCollapsed={this.props.previewCollapsed} - emojis={this.props.emojis} /> </div> </div> @@ -279,7 +274,6 @@ Post.propTypes = { isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, displayNameType: React.PropTypes.string, - hasProfiles: React.PropTypes.bool, currentUser: React.PropTypes.object.isRequired, center: React.PropTypes.bool, compactDisplay: React.PropTypes.bool, @@ -287,7 +281,6 @@ Post.propTypes = { commentCount: React.PropTypes.number, isCommentMention: React.PropTypes.bool, useMilitaryTime: React.PropTypes.bool.isRequired, - emojis: React.PropTypes.object.isRequired, isFlagged: React.PropTypes.bool, status: React.PropTypes.string }; diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 348e7fc93..8c3b3009f 100644 --- a/webapp/components/post_view/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -6,8 +6,8 @@ import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import Constants from 'utils/constants.jsx'; -import * as TextFormatting from 'utils/text_formatting.jsx'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; +import PostMessageContainer from './post_message_container.jsx'; import PendingPostOptions from './pending_post_options.jsx'; import {FormattedMessage} from 'react-intl'; @@ -43,10 +43,6 @@ export default class PostBody extends React.Component { return true; } - if (nextProps.emojis !== this.props.emojis) { - return true; - } - return false; } @@ -165,10 +161,7 @@ export default class PostBody extends React.Component { ); } else { message = ( - <span - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, {emojis: this.props.emojis})}} - /> + <PostMessageContainer post={this.props.post}/> ); } @@ -215,6 +208,5 @@ PostBody.propTypes = { retryPost: React.PropTypes.func.isRequired, handleCommentClick: React.PropTypes.func.isRequired, compactDisplay: React.PropTypes.bool, - previewCollapsed: React.PropTypes.string, - emojis: React.PropTypes.object.isRequired + previewCollapsed: React.PropTypes.string }; diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index fc532c373..f891afdb3 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -314,7 +314,6 @@ export default class PostList extends React.Component { compactDisplay={this.props.compactDisplay} previewCollapsed={this.props.previewsCollapsed} useMilitaryTime={this.props.useMilitaryTime} - emojis={this.props.emojis} isFlagged={isFlagged} status={status} /> @@ -584,7 +583,6 @@ PostList.propTypes = { previewsCollapsed: React.PropTypes.string, useMilitaryTime: React.PropTypes.bool.isRequired, isFocusPost: React.PropTypes.bool, - emojis: React.PropTypes.object.isRequired, flaggedPosts: React.PropTypes.object, statuses: React.PropTypes.object }; diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx new file mode 100644 index 000000000..4ab556fca --- /dev/null +++ b/webapp/components/post_view/components/post_message_container.jsx @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import EmojiStore from 'stores/emoji_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; +import {Preferences} from 'utils/constants.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import PostMessageView from './post_message_view.jsx'; + +export default class PostMessageContainer extends React.Component { + static propTypes = { + post: React.PropTypes.object.isRequired, + options: React.PropTypes.object + }; + + static defaultProps = { + options: {} + }; + + constructor(props) { + super(props); + + this.onEmojiChange = this.onEmojiChange.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); + + const mentionKeys = UserStore.getCurrentMentionKeys(); + mentionKeys.push('@here'); + + this.state = { + emojis: EmojiStore.getEmojis(), + enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true), + mentionKeys, + usernameMap: UserStore.getProfilesUsernameMap() + }; + } + + componentDidMount() { + EmojiStore.addChangeListener(this.onEmojiChange); + PreferenceStore.addChangeListener(this.onPreferenceChange); + UserStore.addChangeListener(this.onUserChange); + } + + componentWillUnmount() { + EmojiStore.removeChangeListener(this.onEmojiChange); + PreferenceStore.removeChangeListener(this.onPreferenceChange); + UserStore.removeChangeListener(this.onUserChange); + } + + onEmojiChange() { + this.setState({ + emojis: EmojiStore.getEmojis() + }); + } + + onPreferenceChange() { + this.setState({ + enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true) + }); + } + + onUserChange() { + const mentionKeys = UserStore.getCurrentMentionKeys(); + mentionKeys.push('@here'); + + this.setState({ + mentionKeys, + usernameMap: UserStore.getProfilesUsernameMap() + }); + } + + render() { + return ( + <PostMessageView + options={this.props.options} + message={this.props.post.message} + emojis={this.state.emojis} + enableFormatting={this.state.enableFormatting} + mentionKeys={this.state.mentionKeys} + usernameMap={this.state.usernameMap} + /> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx new file mode 100644 index 000000000..99589c973 --- /dev/null +++ b/webapp/components/post_view/components/post_message_view.jsx @@ -0,0 +1,66 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as TextFormatting from 'utils/text_formatting.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class PostMessageView extends React.Component { + static propTypes = { + options: React.PropTypes.object.isRequired, + message: React.PropTypes.string.isRequired, + emojis: React.PropTypes.object.isRequired, + enableFormatting: React.PropTypes.bool.isRequired, + mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + usernameMap: React.PropTypes.object.isRequired + }; + + shouldComponentUpdate(nextProps) { + if (!Utils.areObjectsEqual(nextProps.options, this.props.options)) { + return true; + } + + if (nextProps.message !== this.props.message) { + return true; + } + + // emojis are immutable + if (nextProps.emojis !== this.props.emojis) { + return true; + } + + if (nextProps.enableFormatting !== this.props.enableFormatting) { + return true; + } + + if (!Utils.areObjectsEqual(nextProps.mentionKeys, this.props.mentionKeys)) { + return true; + } + + // Don't check if props.usernameMap changes since it is very large and inefficient to do so. + // This mimics previous behaviour, but could be changed if we decide it's worth it. + + return false; + } + + render() { + if (!this.props.enableFormatting) { + return <span>{this.props.message}</span>; + } + + const options = Object.assign({}, this.props.options, { + emojis: this.props.emojis, + siteURL: Utils.getSiteURL(), + mentionKeys: this.props.mentionKeys, + usernameMap: this.props.usernameMap + }); + + return ( + <span + onClick={Utils.handleFormattedTextClick} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}} + /> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 7e30818fb..58f8eff74 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -4,7 +4,6 @@ import PostList from './components/post_list.jsx'; import LoadingScreen from 'components/loading_screen.jsx'; -import EmojiStore from 'stores/emoji_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PostStore from 'stores/post_store.jsx'; @@ -25,7 +24,6 @@ export default class PostViewController extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); - this.onEmojisChange = this.onEmojisChange.bind(this); this.onStatusChange = this.onStatusChange.bind(this); this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this); this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this); @@ -67,7 +65,6 @@ export default class PostViewController extends React.Component { compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'), useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), - emojis: EmojiStore.getEmojis(), flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }; } @@ -123,12 +120,6 @@ export default class PostViewController extends React.Component { }); } - onEmojisChange() { - this.setState({ - emojis: EmojiStore.getEmojis() - }); - } - onStatusChange() { const channel = this.state.channel; let statuses; @@ -145,7 +136,6 @@ export default class PostViewController extends React.Component { UserStore.addStatusesChangeListener(this.onStatusChange); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); - EmojiStore.addChangeListener(this.onEmojisChange); ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator); } @@ -155,7 +145,6 @@ export default class PostViewController extends React.Component { UserStore.removeStatusesChangeListener(this.onStatusChange); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); - EmojiStore.removeChangeListener(this.onEmojisChange); ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator); } @@ -298,10 +287,6 @@ export default class PostViewController extends React.Component { return true; } - if (nextState.emojis !== this.state.emojis) { - return true; - } - return false; } @@ -332,7 +317,6 @@ export default class PostViewController extends React.Component { useMilitaryTime={this.state.useMilitaryTime} flaggedPosts={this.state.flaggedPosts} lastViewed={this.state.lastViewed} - emojis={this.state.emojis} ownNewMessage={this.state.ownNewMessage} statuses={this.state.statuses} /> diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 05df1ac5f..c9588eb33 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -4,6 +4,7 @@ import UserProfile from './user_profile.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx'; +import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import ProfilePicture from 'components/profile_picture.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -12,7 +13,6 @@ import UserStore from 'stores/user_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {flagPost, unflagPost} from 'actions/post_actions.jsx'; -import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -234,13 +234,7 @@ export default class RhsComment extends React.Component { } let loading; let postClass = ''; - let message = ( - <div - ref='message_holder' - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}} - /> - ); + let message = <PostMessageContainer post={post}/>; if (post.state === Constants.POST_FAILED) { postClass += ' post-fail'; diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index cbb000922..ea0c71cc7 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -3,6 +3,7 @@ import UserProfile from './user_profile.jsx'; import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; +import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import ProfilePicture from 'components/profile_picture.jsx'; @@ -15,7 +16,6 @@ import {flagPost, unflagPost} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; -import * as TextFormatting from 'utils/text_formatting.jsx'; import Constants from 'utils/constants.jsx'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; @@ -305,13 +305,7 @@ export default class RhsRootPost extends React.Component { profilePicContainer = ''; } - const messageWrapper = ( - <div - ref='message_holder' - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}} - /> - ); + const messageWrapper = <PostMessageContainer post={post}/>; let flag; let flagFunc; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index ada5e0ea6..2260f99ad 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import $ from 'jquery'; +import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import UserProfile from './user_profile.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -11,7 +12,6 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {flagPost, unflagPost} from 'actions/post_actions.jsx'; -import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -78,11 +78,6 @@ export default class SearchResultsItem extends React.Component { } } - const formattingOptions = { - searchTerm: this.props.term, - mentionHighlight: this.props.isMentionSearch - }; - let overrideUsername; let disableProfilePopover = false; if (post.props && @@ -251,9 +246,12 @@ export default class SearchResultsItem extends React.Component { </li> </ul> <div className='search-item-snippet'> - <span - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message, formattingOptions)}} + <PostMessageContainer + post={post} + options={{ + searchTerm: this.props.term, + mentionHighlight: this.props.isMentionSearch + }} /> </div> </div> |