From 9ce6ab108a3b7c2ffe65fbbbdeaa73108ea8eafd Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 16 Aug 2017 22:07:25 +0700 Subject: [PATCH] Refactor StreamTabPanel and use hoistStatics for all hocs --- .../src/components/Comment.js | 2 +- .../src/components/Stream.js | 131 ++++++------------ .../src/components/StreamTabPanel.js | 23 +++ .../src/containers/Stream.js | 8 +- .../src/containers/StreamTabPanel.js | 84 +++++++++++ .../hocs/withCopyToClipboard.js | 5 +- client/coral-framework/hocs/withEmit.js | 7 +- client/coral-framework/hocs/withFragments.js | 24 +++- client/coral-framework/hocs/withMutation.js | 5 +- client/coral-framework/hocs/withQuery.js | 5 +- plugin-api/beta/client/hocs/withTags.js | 5 +- 11 files changed, 195 insertions(+), 104 deletions(-) create mode 100644 client/coral-embed-stream/src/components/StreamTabPanel.js create mode 100644 client/coral-embed-stream/src/containers/StreamTabPanel.js diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6c231f4ae..b8063858d 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -72,7 +72,7 @@ const ActionButton = ({children}) => { ); }; -export default class Comment extends React.Component { +export default class Comment extends React.PureComponent { constructor(props) { super(props); diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 7b9e5f566..a9ea98a68 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -10,16 +10,16 @@ import {ModerationLink} from 'talk-plugin-moderation'; import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; import t, {timeago} from 'coral-framework/services/i18n'; -import {getSlotComponents} from 'coral-framework/helpers/plugins'; import CommentBox from 'talk-plugin-commentbox/CommentBox'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; import {isCommentActive} from 'coral-framework/utils'; -import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui'; +import {Button, Tab, TabCount, TabPane} from 'coral-ui'; import cn from 'classnames'; import {getTopLevelParent, attachCommentToParent} from '../graphql/utils'; import AllCommentsPane from './AllCommentsPane'; import AutomaticAssetClosure from '../containers/AutomaticAssetClosure'; +import StreamTabPanel from '../containers/StreamTabPanel'; import styles from './Stream.css'; @@ -35,44 +35,9 @@ class Stream extends React.Component { componentWillReceiveProps(next) { // Keep comment box when user was live suspended, banned, ... - if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) { + if (!this.props.userIsDegraged && next.userIsDegraged) { this.setState({keepCommentBox: true}); } - - this.fallbackAllTab(next); - } - - componentDidMount() { - this.fallbackAllTab(); - } - - fallbackAllTab(props = this.props) { - if (props.activeStreamTab !== 'all') { - const slotPlugins = this.getSlotComponents('streamTabs', props).map((c) => c.talkPluginName); - if (slotPlugins.indexOf(props.activeStreamTab) === -1) { - props.setActiveStreamTab('all'); - } - } - } - - getSlotProps({data, root, root: {asset}} = this.props) { - return {data, root, asset}; - } - - getSlotComponents(slot, props = this.props) { - return getSlotComponents(slot, props.reduxState, this.getSlotProps(props)); - } - - setActiveReplyBox = (id) => { - if (!this.props.auth.user) { - this.props.showSignInDialog(); - } else { - this.props.setActiveReplyBox(id); - } - }; - - userIsDegraged({auth: {user}} = this.props) { - return !can(user, 'INTERACT_WITH_COMMUNITY'); } render() { @@ -99,7 +64,7 @@ class Stream extends React.Component { loadMoreComments, viewAllComments, auth: {loggedIn, user}, - editName + editName, } = this.props; const {keepCommentBox} = this.state; const open = !asset.isClosed; @@ -133,7 +98,7 @@ class Stream extends React.Component { }; const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox); - const slotProps = this.getSlotProps(); + const slotProps = {data, root, asset}; if (!comment && !comments) { console.error('Talk: No comments came back from the graph given that query. Please, check the query params.'); @@ -247,55 +212,49 @@ class Stream extends React.Component { {...slotProps} /> - - {this.getSlotComponents('streamTabs').map((PluginComponent) => ( - - + + All Comments {totalCommentCount} - ))} - - All Comments {totalCommentCount} - - - - {this.getSlotComponents('streamTabPanes').map((PluginComponent) => ( - - + - ))} - - - - + } + sub + /> } diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/StreamTabPanel.js new file mode 100644 index 000000000..c5a292179 --- /dev/null +++ b/client/coral-embed-stream/src/components/StreamTabPanel.js @@ -0,0 +1,23 @@ +import React from 'react'; +import {TabBar, TabContent} from 'coral-ui'; + +class StreamTabPanel extends React.Component { + + render() { + const {activeTab, setActiveTab, appendTabs, appendTabPanes, pluginTabElements, pluginTabPaneElements, sub} = this.props; + return ( +
+ + {pluginTabElements} + {appendTabs} + + + {pluginTabPaneElements} + {appendTabPanes} + +
+ ); + } +} + +export default StreamTabPanel; diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 451b115a9..acc178261 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -16,6 +16,7 @@ import Comment from './Comment'; import {withFragments, withEmit} from 'coral-framework/hocs'; import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {Spinner} from 'coral-ui'; +import {can} from 'coral-framework/services/perms'; import { findCommentInEmbedQuery, insertCommentIntoEmbedQuery, @@ -23,7 +24,6 @@ import { insertFetchedCommentsIntoEmbedQuery, nest, } from '../graphql/utils'; -import omit from 'lodash/omit'; const {showSignInDialog, editName} = authActions; const {addNotification} = notificationActions; @@ -140,6 +140,10 @@ class StreamContainer extends React.Component { clearInterval(this.countPoll); } + userIsDegraged({auth: {user}} = this.props) { + return !can(user, 'INTERACT_WITH_COMMUNITY'); + } + render() { if (this.props.refetching) { return ; @@ -149,6 +153,7 @@ class StreamContainer extends React.Component { loadMore={this.loadMore} loadMoreComments={this.loadMoreComments} loadNewReplies={this.loadNewReplies} + userIsDegraged={this.userIsDegraged()} />; } } @@ -310,7 +315,6 @@ const mapStateToProps = (state) => ({ previousStreamTab: state.stream.previousTab, commentClassNames: state.stream.commentClassNames, pluginConfig: state.config.plugin_config, - reduxState: omit(state, 'apollo'), }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js new file mode 100644 index 000000000..574c4c881 --- /dev/null +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -0,0 +1,84 @@ +import React from 'react'; +import StreamTabPanel from '../components/StreamTabPanel'; +import {connect} from 'react-redux'; +import omit from 'lodash/omit'; +import {getSlotComponents} from 'coral-framework/helpers/plugins'; +import {Tab, TabPane} from 'coral-ui'; +import {getShallowChanges} from 'coral-framework/utils'; +import isEqual from 'lodash/isEqual'; + +class StreamTabPanelContainer extends React.Component { + + componentDidMount() { + this.fallbackAllTab(); + } + + componentWillReceiveProps(next) { + this.fallbackAllTab(next); + } + + shouldComponentUpdate(next) { + + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change of slot children. + const changes = getShallowChanges(this.props, next); + if (changes.length === 1 && changes[0] === 'reduxState') { + const prevUuid = this.getSlotComponents(this.props.tabSlot, this.props).map((cmp) => cmp.talkUuid); + const nextUuid = this.getSlotComponents(next.tabSlot, next).map((cmp) => cmp.talkUuid); + return !isEqual(prevUuid, nextUuid); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + fallbackAllTab(props = this.props) { + if (props.activeTab !== props.fallbackTab) { + const slotPlugins = this.getSlotComponents(props.tabSlot, props).map((c) => c.talkPluginName); + if (slotPlugins.indexOf(props.activeTab) === -1) { + props.setActiveTab(props.fallbackTab); + } + } + } + + getSlotComponents(slot, props = this.props) { + return getSlotComponents(slot, props.reduxState, props.slotProps); + } + + getPluginTabElements(props = this.props) { + return this.getSlotComponents(props.tabSlot).map((PluginComponent) => ( + + + + )); + } + + getPluginTabPaneElements(props = this.props) { + return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => ( + + + + )); + } + + render() { + return ( + + ); + } +} + +const mapStateToProps = (state) => ({ + reduxState: omit(state, 'apollo'), +}); + +export default connect(mapStateToProps, null)(StreamTabPanelContainer); diff --git a/client/coral-framework/hocs/withCopyToClipboard.js b/client/coral-framework/hocs/withCopyToClipboard.js index 9e8046802..4de606c40 100644 --- a/client/coral-framework/hocs/withCopyToClipboard.js +++ b/client/coral-framework/hocs/withCopyToClipboard.js @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Clipboard from 'clipboard'; +import hoistStatics from 'recompose/hoistStatics'; -export default (WrappedComponent) => { +export default hoistStatics((WrappedComponent) => { class WithCopyToClipboard extends React.Component { componentDidMount() { const clipboard = new Clipboard(ReactDOM.findDOMNode(this)); @@ -26,4 +27,4 @@ export default (WrappedComponent) => { } return WithCopyToClipboard; -}; +}); diff --git a/client/coral-framework/hocs/withEmit.js b/client/coral-framework/hocs/withEmit.js index 3a3216c8a..22d98bb27 100644 --- a/client/coral-framework/hocs/withEmit.js +++ b/client/coral-framework/hocs/withEmit.js @@ -1,11 +1,12 @@ import React from 'react'; -const PropTypes = require('prop-types'); +import hoistStatics from 'recompose/hoistStatics'; +import PropTypes from 'prop-types'; /** * WithEmit provides a property `emit: (eventName, value)` * to the wrapped component. */ -export default (WrappedComponent) => { +export default hoistStatics((WrappedComponent) => { class WithEmit extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, @@ -24,4 +25,4 @@ export default (WrappedComponent) => { } return WithEmit; -}; +}); diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 62e96444c..c422f8705 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,5 +1,21 @@ // TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. -export default (fragments) => (BaseComponent) => { - BaseComponent.fragments = fragments; - return BaseComponent; -}; + +import React from 'react'; +import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; +import mapValues from 'lodash/mapValues'; +import hoistStatics from 'recompose/hoistStatics'; + +export default (fragments) => hoistStatics((BaseComponent) => { + class WithFragments extends React.Component { + fragments = mapValues(fragments, (val) => resolveFragments(val)); + + render() { + return ; + } + } + + WithFragments.fragments = fragments; + return WithFragments; +}); diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 7e2c3d09d..bb2321d11 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -8,6 +8,7 @@ import {getMutationOptions, resolveFragments} from 'coral-framework/services/gra import {getDefinitionName, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; +import hoistStatics from 'recompose/hoistStatics'; class ResponseErrors extends Error { constructor(errors) { @@ -30,7 +31,7 @@ class ResponseError { * Exports a HOC with the same signature as `graphql`, that will * apply mutation options registered in the graphRegistry. */ -export default (document, config = {}) => (WrappedComponent) => { +export default (document, config = {}) => hoistStatics((WrappedComponent) => { config = { ...config, options: config.options || {}, @@ -147,4 +148,4 @@ export default (document, config = {}) => (WrappedComponent) => { return ; } }; -}; +}); diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index d98400419..f19c3209e 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -3,6 +3,7 @@ import {graphql} from 'react-apollo'; import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry'; import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; +import hoistStatics from 'recompose/hoistStatics'; const withSkipOnErrors = (reducer) => (prev, action, ...rest) => { if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) { @@ -35,7 +36,7 @@ function networkStatusToString(networkStatus) { * Exports a HOC with the same signature as `graphql`, that will * apply query options registered in the graphRegistry. */ -export default (document, config = {}) => (WrappedComponent) => { +export default (document, config = {}) => hoistStatics((WrappedComponent) => { const name = getDefinitionName(document); return class WithQuery extends React.Component { @@ -190,4 +191,4 @@ export default (document, config = {}) => (WrappedComponent) => { return ; } }; -}; +}); diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index b0a74f4d7..428c3f43f 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -8,8 +8,9 @@ import {withAddTag, withRemoveTag} from 'coral-framework/graphql/mutations'; import withFragments from 'coral-framework/hocs/withFragments'; import {addNotification} from 'coral-framework/actions/notification'; import {forEachError, isTagged} from 'coral-framework/utils'; +import hoistStatics from 'recompose/hoistStatics'; -export default (tag) => (WrappedComponent) => { +export default (tag) => hoistStatics((WrappedComponent) => { if (typeof tag !== 'string') { console.error('Tag must be a valid string'); return null; @@ -109,4 +110,4 @@ export default (tag) => (WrappedComponent) => { WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`; return enhance(WithTags); -}; +});