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
Этот коммит содержится в:
Harrison Healey
2016-08-29 09:50:00 -04:00
коммит произвёл Christopher Speller
родитель 55342e8fe1
Коммит 167dd22eef
20 изменённых файлов: 765 добавлений и 142 удалений

Просмотреть файл

@@ -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}
/>
);
}
}

Просмотреть файл

@@ -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}
/>