diff options
author | Joram Wilander <jwawilander@gmail.com> | 2016-04-29 08:40:06 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-04-29 08:40:06 -0400 |
commit | 1f4974dc02c786b65c802d4497fd736cca79d01c (patch) | |
tree | 1007e452c4a9345dee8aff113f28f235432bf323 | |
parent | 9961ccca7d39bdfabbafce423d3f7fe4b6ed2f29 (diff) | |
download | chat-1f4974dc02c786b65c802d4497fd736cca79d01c.tar.gz chat-1f4974dc02c786b65c802d4497fd736cca79d01c.tar.bz2 chat-1f4974dc02c786b65c802d4497fd736cca79d01c.zip |
General react performance improvements (#2796)
* General React performance improvements
* Cleaned up unused props/state in PermaLinkView and PostFocusView
-rw-r--r-- | webapp/components/channel_info_modal.jsx | 13 | ||||
-rw-r--r-- | webapp/components/channel_view.jsx | 18 | ||||
-rw-r--r-- | webapp/components/floating_timestamp.jsx | 54 | ||||
-rw-r--r-- | webapp/components/get_post_link_modal.jsx | 33 | ||||
-rw-r--r-- | webapp/components/get_team_invite_link_modal.jsx | 41 | ||||
-rw-r--r-- | webapp/components/more_channels.jsx | 17 | ||||
-rw-r--r-- | webapp/components/more_direct_channels.jsx | 21 | ||||
-rw-r--r-- | webapp/components/needs_team.jsx | 7 | ||||
-rw-r--r-- | webapp/components/permalink_view.jsx | 8 | ||||
-rw-r--r-- | webapp/components/post_body.jsx | 47 | ||||
-rw-r--r-- | webapp/components/post_focus_view.jsx | 21 | ||||
-rw-r--r-- | webapp/components/posts_view.jsx | 98 | ||||
-rw-r--r-- | webapp/components/posts_view_container.jsx | 22 | ||||
-rw-r--r-- | webapp/components/rhs_comment.jsx | 39 | ||||
-rw-r--r-- | webapp/components/sidebar.jsx | 5 | ||||
-rw-r--r-- | webapp/components/sidebar_right_menu.jsx | 3 | ||||
-rw-r--r-- | webapp/components/time_since.jsx | 6 | ||||
-rw-r--r-- | webapp/components/user_profile.jsx | 19 | ||||
-rw-r--r-- | webapp/package.json | 6 |
19 files changed, 253 insertions, 225 deletions
diff --git a/webapp/components/channel_info_modal.jsx b/webapp/components/channel_info_modal.jsx index 4c16dda90..d44df4056 100644 --- a/webapp/components/channel_info_modal.jsx +++ b/webapp/components/channel_info_modal.jsx @@ -9,6 +9,17 @@ import {Modal} from 'react-bootstrap'; import React from 'react'; export default class ChannelInfoModal extends React.Component { + shouldComponentUpdate(nextProps) { + if (nextProps.show !== this.props.show) { + return true; + } + + if (!Utils.areObjectsEqual(nextProps.channel, this.props.channel)) { + return true; + } + + return false; + } render() { let channel = this.props.channel; if (!channel) { @@ -93,4 +104,4 @@ ChannelInfoModal.propTypes = { show: React.PropTypes.bool.isRequired, onHide: React.PropTypes.func.isRequired, channel: React.PropTypes.object.isRequired -};
\ No newline at end of file +}; diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx index 45d0f2393..6511d960a 100644 --- a/webapp/components/channel_view.jsx +++ b/webapp/components/channel_view.jsx @@ -11,6 +11,8 @@ import PostsViewContainer from 'components/posts_view_container.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + export default class ChannelView extends React.Component { constructor(props) { super(props); @@ -47,6 +49,17 @@ export default class ChannelView extends React.Component { componentWillReceiveProps(nextProps) { this.setState(this.getStateFromStores(nextProps)); } + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(nextProps.params, this.props.params)) { + return true; + } + + if (nextState.channelId !== this.state.channelId) { + return true; + } + + return false; + } render() { return ( <div @@ -57,7 +70,7 @@ export default class ChannelView extends React.Component { <ChannelHeader channelId={this.state.channelId} /> - <PostsViewContainer profiles={this.props.profiles}/> + <PostsViewContainer/> <div className='post-create__container' id='post-create' @@ -72,6 +85,5 @@ ChannelView.defaultProps = { }; ChannelView.propTypes = { - params: React.PropTypes.object.isRequired, - profiles: React.PropTypes.object + params: React.PropTypes.object.isRequired }; diff --git a/webapp/components/floating_timestamp.jsx b/webapp/components/floating_timestamp.jsx new file mode 100644 index 000000000..8974c62c5 --- /dev/null +++ b/webapp/components/floating_timestamp.jsx @@ -0,0 +1,54 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedDate} from 'react-intl'; + +import React from 'react'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +export default class FloatingTimestamp extends React.Component { + constructor(props) { + super(props); + + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + } + + render() { + if (!this.props.isMobile) { + return <noscript/>; + } + + if (this.props.createAt === 0) { + return <noscript/>; + } + + const dateString = ( + <FormattedDate + value={this.props.createAt} + weekday='short' + day='2-digit' + month='short' + year='numeric' + /> + ); + + let className = 'post-list__timestamp'; + if (this.props.isScrolling) { + className += ' scrolling'; + } + + return ( + <div className={className}> + <div> + <span>{dateString}</span> + </div> + </div> + ); + } +} + +FloatingTimestamp.propTypes = { + isScrolling: React.PropTypes.bool.isRequired, + isMobile: React.PropTypes.bool, + createAt: React.PropTypes.number +}; diff --git a/webapp/components/get_post_link_modal.jsx b/webapp/components/get_post_link_modal.jsx index 4c56d4d64..cc01d1124 100644 --- a/webapp/components/get_post_link_modal.jsx +++ b/webapp/components/get_post_link_modal.jsx @@ -1,34 +1,25 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import Constants from 'utils/constants.jsx'; import GetLinkModal from './get_link_modal.jsx'; import ModalStore from 'stores/modal_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import {intlShape, injectIntl, defineMessages} from 'react-intl'; - -const holders = defineMessages({ - title: { - id: 'get_post_link_modal.title', - defaultMessage: 'Copy Permalink' - }, - help: { - id: 'get_post_link_modal.help', - defaultMessage: 'The link below allows authorized users to see your post.' - } -}); +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; -class GetPostLinkModal extends React.Component { +export default class GetPostLinkModal extends React.Component { constructor(props) { super(props); this.handleToggle = this.handleToggle.bind(this); - this.hide = this.hide.bind(this); + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + this.state = { show: false, post: {} @@ -57,22 +48,14 @@ class GetPostLinkModal extends React.Component { } render() { - const {formatMessage} = this.props.intl; - return ( <GetLinkModal show={this.state.show} onHide={this.hide} - title={formatMessage(holders.title)} - helpText={formatMessage(holders.help)} + title={Utils.localizeMessage('get_post_link_modal.title', 'Copy Permalink')} + helpText={Utils.localizeMessage('get_post_link_modal.help', 'The link below allows authorized users to see your post.')} link={TeamStore.getCurrentTeamUrl() + '/pl/' + this.state.post.id} /> ); } } - -GetPostLinkModal.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(GetPostLinkModal); diff --git a/webapp/components/get_team_invite_link_modal.jsx b/webapp/components/get_team_invite_link_modal.jsx index 33cc065d1..109cb2120 100644 --- a/webapp/components/get_team_invite_link_modal.jsx +++ b/webapp/components/get_team_invite_link_modal.jsx @@ -1,36 +1,24 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import Constants from 'utils/constants.jsx'; import GetLinkModal from './get_link_modal.jsx'; import ModalStore from 'stores/modal_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import {intlShape, injectIntl, defineMessages} from 'react-intl'; - -const holders = defineMessages({ - title: { - id: 'get_team_invite_link_modal.title', - defaultMessage: 'Team Invite Link' - }, - help: { - id: 'get_team_invite_link_modal.help', - defaultMessage: 'Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it\'s regenerated in Team Settings by a Team Admin.' - }, - helpDisabled: { - id: 'get_team_invite_link_modal.helpDisabled', - defaultMessage: 'User creation has been disabled for your team. Please ask your team administrator for details.' - } -}); +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; -class GetTeamInviteLinkModal extends React.Component { +export default class GetTeamInviteLinkModal extends React.Component { constructor(props) { super(props); this.handleToggle = this.handleToggle.bind(this); + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + this.state = { show: false }; @@ -51,28 +39,21 @@ class GetTeamInviteLinkModal extends React.Component { } render() { - const {formatMessage} = this.props.intl; - - let helpText = formatMessage(holders.helpDisabled); - + let helpText; if (global.window.mm_config.EnableUserCreation === 'true') { - helpText = formatMessage(holders.help); + helpText = Utils.localizeMessage('get_team_invite_link_modal.help', 'Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it\'s regenerated in Team Settings by a Team Admin.'); + } else { + helpText = Utils.localizeMessage('get_team_invite_link_modal.helpDisabled', 'User creation has been disabled for your team. Please ask your team administrator for details.'); } return ( <GetLinkModal show={this.state.show} onHide={() => this.setState({show: false})} - title={formatMessage(holders.title)} + title={Utils.localizeMessage('get_team_invite_link_modal.title', 'Team Invite Link')} helpText={helpText} link={TeamStore.getCurrentInviteLink()} /> ); } } - -GetTeamInviteLinkModal.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(GetTeamInviteLinkModal); diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index 04c613ce5..087db68e6 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -2,17 +2,22 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; +import LoadingScreen from './loading_screen.jsx'; +import NewChannelFlow from './new_channel_flow.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; + import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import LoadingScreen from './loading_screen.jsx'; -import NewChannelFlow from './new_channel_flow.jsx'; import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; + import loadingGif from 'images/load.gif'; function getStateFromStores() { @@ -22,8 +27,6 @@ function getStateFromStores() { }; } -import React from 'react'; - export default class MoreChannels extends React.Component { constructor(props) { super(props); @@ -33,6 +36,8 @@ export default class MoreChannels extends React.Component { this.handleNewChannel = this.handleNewChannel.bind(this); this.createChannelRow = this.createChannelRow.bind(this); + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + var initState = getStateFromStores(); initState.channelType = ''; initState.joiningChannel = -1; diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index a83816c40..a7fb2b6cd 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -19,7 +19,6 @@ export default class MoreDirectChannels extends React.Component { this.handleHide = this.handleHide.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); this.handleUserChange = this.handleUserChange.bind(this); - this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this); this.state = { @@ -52,6 +51,26 @@ export default class MoreDirectChannels extends React.Component { UserStore.removeChangeListener(this.handleUserChange); } + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.show !== this.props.show) { + return true; + } + + if (nextProps.onModalDismissed.toString() !== this.props.onModalDismissed.toString()) { + return true; + } + + if (nextState.loadingDMChannel !== this.state.loadingDMChannel) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.users, this.state.users)) { + return true; + } + + return false; + } + handleHide() { if (this.props.onModalDismissed) { this.props.onModalDismissed(); diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 4aea8fe46..92c6fc0ce 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -42,20 +42,18 @@ export default class NeedsTeam extends React.Component { this.onChanged = this.onChanged.bind(this); this.state = { - profiles: UserStore.getProfiles(), team: TeamStore.getCurrent() }; } onChanged() { this.setState({ - profiles: UserStore.getProfiles(), team: TeamStore.getCurrent() }); } componentWillMount() { - // Go to tutorial if we are first arrivign + // Go to tutorial if we are first arriving const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); if (tutorialStep <= TutorialSteps.INTRO_SCREENS) { browserHistory.push(Utils.getTeamURLNoOriginFromAddressBar() + '/tutorial'); @@ -63,7 +61,6 @@ export default class NeedsTeam extends React.Component { } componentDidMount() { - UserStore.addChangeListener(this.onChanged); TeamStore.addChangeListener(this.onChanged); // Emit view action @@ -84,7 +81,6 @@ export default class NeedsTeam extends React.Component { } componentWillUnmount() { - UserStore.removeChangeListener(this.onChanged); TeamStore.removeChangeListener(this.onChanged); $(window).off('focus'); $(window).off('blur'); @@ -114,7 +110,6 @@ export default class NeedsTeam extends React.Component { <div className='row main'> {React.cloneElement(this.props.center, { user: this.props.user, - profiles: this.state.profiles, team: this.state.team })} </div> diff --git a/webapp/components/permalink_view.jsx b/webapp/components/permalink_view.jsx index 07f826d57..c2019cb49 100644 --- a/webapp/components/permalink_view.jsx +++ b/webapp/components/permalink_view.jsx @@ -70,7 +70,7 @@ export default class PermalinkView extends React.Component { <ChannelHeader channelId={this.state.channelId} /> - <PostFocusView profiles={this.props.profiles}/> + <PostFocusView/> <div id='archive-link-home' > @@ -89,10 +89,6 @@ export default class PermalinkView extends React.Component { } } -PermalinkView.defaultProps = { -}; - PermalinkView.propTypes = { - params: React.PropTypes.object.isRequired, - profiles: React.PropTypes.object + params: React.PropTypes.object.isRequired }; diff --git a/webapp/components/post_body.jsx b/webapp/components/post_body.jsx index 884dbbbbb..a159dcbb1 100644 --- a/webapp/components/post_body.jsx +++ b/webapp/components/post_body.jsx @@ -10,24 +10,13 @@ import * as TextFormatting from 'utils/text_formatting.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import loadingGif from 'images/load.gif'; -const holders = defineMessages({ - plusOne: { - id: 'post_body.plusOne', - defaultMessage: ' plus 1 other file' - }, - plusMore: { - id: 'post_body.plusMore', - defaultMessage: ' plus {count} other files' - } -}); - import React from 'react'; -class PostBody extends React.Component { +export default class PostBody extends React.Component { constructor(props) { super(props); @@ -61,8 +50,27 @@ class PostBody extends React.Component { this.parseEmojis(); } + shouldComponentUpdate(nextProps) { + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { + return true; + } + + if (!Utils.areObjectsEqual(nextProps.parentPost, this.props.parentPost)) { + return true; + } + + if (nextProps.retryPost.toString() !== this.props.retryPost.toString()) { + return true; + } + + if (nextProps.handleCommentClick.toString() !== this.props.handleCommentClick.toString()) { + return true; + } + + return false; + } + render() { - const {formatMessage} = this.props.intl; const post = this.props.post; const filenames = this.props.post.filenames; const parentPost = this.props.parentPost; @@ -106,9 +114,9 @@ class PostBody extends React.Component { message = parentPost.filenames[0].split('/').pop(); if (parentPost.filenames.length === 2) { - message += formatMessage(holders.plusOne); + message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file'); } else if (parentPost.filenames.length > 2) { - message += formatMessage(holders.plusMore, {count: (parentPost.filenames.length - 1)}); + message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (parentPost.filenames.length - 1).toString()); } } @@ -119,8 +127,8 @@ class PostBody extends React.Component { id='post_body.commentedOn' defaultMessage='Commented on {name}{apostrophe} message: ' values={{ - name: (name), - apostrophe: apostrophe + name, + apostrophe }} /> <a @@ -213,11 +221,8 @@ class PostBody extends React.Component { } PostBody.propTypes = { - intl: intlShape.isRequired, post: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func.isRequired, handleCommentClick: React.PropTypes.func.isRequired }; - -export default injectIntl(PostBody); diff --git a/webapp/components/post_focus_view.jsx b/webapp/components/post_focus_view.jsx index f3dfb46c6..d2fbb4532 100644 --- a/webapp/components/post_focus_view.jsx +++ b/webapp/components/post_focus_view.jsx @@ -5,7 +5,6 @@ import PostsView from './posts_view.jsx'; import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; @@ -18,7 +17,6 @@ export default class PostFocusView extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); - this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); @@ -31,21 +29,18 @@ export default class PostFocusView extends React.Component { scrollPostId: focusedPostId, postList: PostStore.getVisiblePosts(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId), - atBottom: PostStore.getVisibilityAtBottom(focusedPostId), - currentUser: UserStore.getCurrentUser() + atBottom: PostStore.getVisibilityAtBottom(focusedPostId) }; } componentDidMount() { ChannelStore.addChangeListener(this.onChannelChange); PostStore.addChangeListener(this.onPostsChange); - UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); PostStore.removeChangeListener(this.onPostsChange); - UserStore.removeChangeListener(this.onUserChange); } onChannelChange() { @@ -58,10 +53,6 @@ export default class PostFocusView extends React.Component { } } - onUserChange() { - this.setState({currentUser: UserStore.getCurrentUser()}); - } - onPostsChange() { const focusedPostId = PostStore.getFocusedPostId(); if (focusedPostId == null) { @@ -105,7 +96,7 @@ export default class PostFocusView extends React.Component { const postsToHighlight = {}; postsToHighlight[this.state.scrollPostId] = true; - if (!this.state.currentUser || !this.state.postList) { + if (!this.state.postList) { return null; } @@ -125,16 +116,8 @@ export default class PostFocusView extends React.Component { introText={this.getIntroMessage()} messageSeparatorTime={0} postsToHighlight={postsToHighlight} - profiles={this.props.profiles} - currentUser={this.state.currentUser} /> </div> ); } } -PostFocusView.defaultProps = { -}; - -PostFocusView.propTypes = { - profiles: React.PropTypes.object -}; diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx index be098086f..327756723 100644 --- a/webapp/components/posts_view.jsx +++ b/webapp/components/posts_view.jsx @@ -2,19 +2,25 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import PreferenceStore from 'stores/preference_store.jsx'; + +import Post from './post.jsx'; +import FloatingTimestamp from './floating_timestamp.jsx'; + import * as GlobalActions from 'action_creators/global_actions.jsx'; + +import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + import * as Utils from 'utils/utils.jsx'; -import Post from './post.jsx'; -import Constants from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; -import {FormattedDate, FormattedMessage} from 'react-intl'; - +import Constants from 'utils/constants.jsx'; const Preferences = Constants.Preferences; +import {FormattedDate, FormattedMessage} from 'react-intl'; + import React from 'react'; +import ReactDOM from 'react-dom'; export default class PostsView extends React.Component { constructor(props) { @@ -31,6 +37,7 @@ export default class PostsView extends React.Component { this.handleResize = this.handleResize.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this); this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.jumpToPostNode = null; this.wasAtBottom = true; @@ -43,7 +50,9 @@ export default class PostsView extends React.Component { centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, isScrolling: false, topPostId: null, - showUnreadMessageAlert: false + showUnreadMessageAlert: false, + currentUser: UserStore.getCurrentUser(), + profiles: UserStore.getProfiles() }; } static get SCROLL_TYPE_FREE() { @@ -67,6 +76,9 @@ export default class PostsView extends React.Component { centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED }); } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser(), profiles: UserStore.getProfiles()}); + } isAtBottom() { // consider the view to be at the bottom if it's within this many pixels of the bottom const atBottomMargin = 10; @@ -152,8 +164,8 @@ export default class PostsView extends React.Component { createPosts(posts, order) { const postCtls = []; let previousPostDay = new Date(0); - const userId = this.props.currentUser.id; - const profiles = this.props.profiles || {}; + const userId = this.state.currentUser.id; + const profiles = this.state.profiles || {}; let renderedLastViewed = false; @@ -229,8 +241,8 @@ export default class PostsView extends React.Component { const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id); let profile; - if (this.props.currentUser.id === post.user_id) { - profile = this.props.currentUser; + if (userId === post.user_id) { + profile = this.state.currentUser; } else { profile = profiles[post.user_id]; } @@ -250,7 +262,7 @@ export default class PostsView extends React.Component { onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} user={profile} - currentUser={this.props.currentUser} + currentUser={this.state.currentUser} center={this.state.centerPosts} /> ); @@ -377,13 +389,19 @@ export default class PostsView extends React.Component { if (this.props.postList != null) { this.updateScrolling(); } + + if (this.props.isActive) { + PreferenceStore.addChangeListener(this.updateState); + UserStore.addChangeListener(this.onUserChange); + } + window.addEventListener('resize', this.handleResize); - PreferenceStore.addChangeListener(this.updateState); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); this.scrollStopAction.cancel(); PreferenceStore.removeChangeListener(this.updateState); + UserStore.removeChangeListener(this.onUserChange); } componentDidUpdate() { if (this.props.postList != null) { @@ -401,8 +419,10 @@ export default class PostsView extends React.Component { if (!this.props.isActive && nextProps.isActive) { this.updateState(); PreferenceStore.addChangeListener(this.updateState); + UserStore.addChangeListener(this.onUserChange); } else if (this.props.isActive && !nextProps.isActive) { PreferenceStore.removeChangeListener(this.updateState); + UserStore.removeChangeListener(this.onUserChange); } } shouldComponentUpdate(nextProps, nextState) { @@ -436,7 +456,7 @@ export default class PostsView extends React.Component { if (this.state.centerPosts !== nextState.centerPosts) { return true; } - if (!Utils.areObjectsEqual(this.props.profiles, nextProps.profiles)) { + if (!Utils.areObjectsEqual(this.state.profiles, nextState.profiles)) { return true; } @@ -497,16 +517,17 @@ export default class PostsView extends React.Component { } } - let topPost = null; + let topPostCreateAt = 0; if (this.state.topPostId) { - topPost = this.props.postList.posts[this.state.topPostId]; + topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at; } return ( <div className={activeClass}> <FloatingTimestamp isScrolling={this.state.isScrolling} - post={topPost} + isMobile={$(window).width() > 768} + createAt={topPostCreateAt} /> <ScrollToBottomArrows isScrolling={this.state.isScrolling} @@ -551,7 +572,6 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -561,47 +581,7 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object, - currentUser: React.PropTypes.object.isRequired -}; - -function FloatingTimestamp({isScrolling, post}) { - // only show on mobile - if ($(window).width() > 768) { - return <noscript/>; - } - - if (!post) { - return <noscript/>; - } - - const dateString = ( - <FormattedDate - value={post.create_at} - weekday='short' - day='2-digit' - month='short' - year='numeric' - /> - ); - - let className = 'post-list__timestamp'; - if (isScrolling) { - className += ' scrolling'; - } - - return ( - <div className={className}> - <div> - <span>{dateString}</span> - </div> - </div> - ); -} - -FloatingTimestamp.propTypes = { - isScrolling: React.PropTypes.bool.isRequired, - post: React.PropTypes.object + postsToHighlight: React.PropTypes.object }; function ScrollToBottomArrows({isScrolling, atBottom, onClick}) { diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx index edfa314f8..32b0aa578 100644 --- a/webapp/components/posts_view_container.jsx +++ b/webapp/components/posts_view_container.jsx @@ -1,12 +1,13 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; + import PostsView from './posts_view.jsx'; import LoadingScreen from './loading_screen.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; -import UserStore from 'stores/user_store.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; @@ -25,7 +26,6 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); - this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -33,8 +33,7 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null, - currentUser: UserStore.getCurrentUser() + scrollPost: null }; if (currentChannelId) { Object.assign(state, { @@ -60,17 +59,14 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); - UserStore.addChangeListener(this.onUserChange); + $('body').addClass('app__body'); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); - UserStore.removeChangeListener(this.onUserChange); - } - onUserChange() { - this.setState({currentUser: UserStore.getCurrentUser()}); + $('body').removeClass('app__body'); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -171,7 +167,7 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); - if (!this.state.currentUser || !channel) { + if (!channel) { return null; } @@ -194,8 +190,6 @@ export default class PostsViewContainer extends React.Component { showMoreMessagesBottom={false} introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} - profiles={this.props.profiles} - currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { @@ -215,7 +209,3 @@ export default class PostsViewContainer extends React.Component { ); } } - -PostsViewContainer.propTypes = { - profiles: React.PropTypes.object -}; diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 709865dc1..290eb64c6 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -1,35 +1,32 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ReactDOM from 'react-dom'; +import UserProfile from './user_profile.jsx'; +import FileAttachmentList from './file_attachment_list.jsx'; + import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import UserProfile from './user_profile.jsx'; + +import * as GlobalActions from 'action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; + +import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; -import FileAttachmentList from './file_attachment_list.jsx'; import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -var ActionTypes = Constants.ActionTypes; -import * as TextFormatting from 'utils/text_formatting.jsx'; -import twemoji from 'twemoji'; -import * as GlobalActions from 'action_creators/global_actions.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'react-intl'; +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; -import loadingGif from 'images/load.gif'; +import {FormattedMessage, FormattedDate} from 'react-intl'; -const holders = defineMessages({ - comment: { - id: 'rhs_comment.comment', - defaultMessage: 'Comment' - } -}); +import loadingGif from 'images/load.gif'; import React from 'react'; +import ReactDOM from 'react-dom'; +import twemoji from 'twemoji'; -class RhsComment extends React.Component { +export default class RhsComment extends React.Component { constructor(props) { super(props); @@ -136,7 +133,7 @@ class RhsComment extends React.Component { data-toggle='modal' data-target='#edit_post' data-refocusid='#reply_textbox' - data-title={this.props.intl.formatMessage(holders.comment)} + data-title={Utils.localizeMessage('rhs_comment.comment', 'Comment')} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id} @@ -302,14 +299,8 @@ class RhsComment extends React.Component { } } -RhsComment.defaultProps = { - post: null -}; RhsComment.propTypes = { - intl: intlShape.isRequired, post: React.PropTypes.object, user: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired }; - -export default injectIntl(RhsComment); diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 2ccf01740..1f73e743f 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -698,8 +698,3 @@ export default class Sidebar extends React.Component { ); } } - -Sidebar.defaultProps = { -}; -Sidebar.propTypes = { -}; diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index 42bc7ce53..b368f9fe1 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -20,6 +20,7 @@ import {Link} from 'react-router'; import {createMenuTip} from 'components/tutorial/tutorial_tip.jsx'; import React from 'react'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; export default class SidebarRightMenu extends React.Component { constructor(props) { @@ -30,6 +31,8 @@ export default class SidebarRightMenu extends React.Component { const state = this.getStateFromStores(); state.showUserSettingsModal = false; + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + this.state = state; } diff --git a/webapp/components/time_since.jsx b/webapp/components/time_since.jsx index 02b0174ae..9f7a93ceb 100644 --- a/webapp/components/time_since.jsx +++ b/webapp/components/time_since.jsx @@ -9,8 +9,14 @@ import {FormattedRelative, FormattedDate} from 'react-intl'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import React from 'react'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; export default class TimeSince extends React.Component { + constructor(props) { + super(props); + + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + } componentDidMount() { this.intervalId = setInterval(() => { this.forceUpdate(); diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx index 04b4f99a4..673422d3f 100644 --- a/webapp/components/user_profile.jsx +++ b/webapp/components/user_profile.jsx @@ -22,6 +22,25 @@ export default class UserProfile extends React.Component { super(props); this.uniqueId = nextId(); } + shouldComponentUpdate(nextProps) { + if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { + return true; + } + + if (nextProps.overwriteName !== this.props.overwriteName) { + return true; + } + + if (nextProps.overwriteImage !== this.props.overwriteImage) { + return true; + } + + if (nextProps.disablePopover !== this.props.disablePopover) { + return true; + } + + return false; + } render() { let name = '...'; let email = ''; diff --git a/webapp/package.json b/webapp/package.json index 3ab971c09..2fa829045 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -21,15 +21,16 @@ "object-assign": "4.0.1", "perfect-scrollbar": "0.6.10", "react": "0.14.7", + "react-addons-pure-render-mixin": "0.14.7", "react-bootstrap": "0.28.3", "react-custom-scrollbars": "^4.0.0-beta.1", "react-dom": "0.14.7", "react-intl": "2.0.0-rc-1", "react-router": "2.0.1", "react-textarea-autosize": "3.3.0", + "superagent": "1.8.3", "twemoji": "1.4.1", - "velocity-animate": "1.2.3", - "superagent": "1.8.3" + "velocity-animate": "1.2.3" }, "devDependencies": { "babel-eslint": "5.0.0", @@ -55,7 +56,6 @@ "style-loader": "0.13.0", "url-loader": "0.5.7", "webpack": "2.1.0-beta.5", - "mocha": "*", "mocha-webpack": "*", "webpack-node-externals": "*", |