summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-08-29 09:50:00 -0400
committerChristopher Speller <crspeller@gmail.com>2016-08-29 09:50:00 -0400
commit167dd22eefeeeb9c1eaebd990a4f5902bd366302 (patch)
tree6ddb15a80b2a608d42e20df72b98c0ae72821671 /webapp/components
parent55342e8fe16613f06528ed1aa726231e9b597d26 (diff)
downloadchat-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.jsx4
-rw-r--r--webapp/components/message_wrapper.jsx13
-rw-r--r--webapp/components/post_view/components/post.jsx7
-rw-r--r--webapp/components/post_view/components/post_body.jsx14
-rw-r--r--webapp/components/post_view/components/post_list.jsx2
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx87
-rw-r--r--webapp/components/post_view/components/post_message_view.jsx66
-rw-r--r--webapp/components/post_view/post_view_controller.jsx16
-rw-r--r--webapp/components/rhs_comment.jsx10
-rw-r--r--webapp/components/rhs_root_post.jsx10
-rw-r--r--webapp/components/search_results_item.jsx16
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>