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);
-};
+});