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
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
55342e8fe1
Коммит
167dd22eef
@@ -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>
|
||||
|
||||
@@ -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/>;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
66
webapp/components/post_view/components/post_message_view.jsx
Обычный файл
66
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)}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Ссылка в новой задаче
Block a user