diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js
index 39c3b5edf..cc1ee7bed 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/components/Comment.js
@@ -224,6 +224,10 @@ export default class Comment extends React.Component {
return;
}
+ commentPostedHandler = () => {
+ this.props.setActiveReplyBox('');
+ }
+
// getVisibileReplies returns a list containing comments
// which were authored by current user or comes before the `idCursor`.
getVisibileReplies() {
@@ -372,10 +376,13 @@ export default class Comment extends React.Component {
// props that are passed down the slots.
const slotProps = {
data,
+ depth,
+ };
+
+ const queryData = {
root,
asset,
comment,
- depth,
};
return (
@@ -389,6 +396,7 @@ export default class Comment extends React.Component {
className={`${styles.commentAvatar} talk-stream-comment-avatar`}
fill="commentAvatar"
{...slotProps}
+ queryData={queryData}
inline
/>
@@ -411,6 +419,7 @@ export default class Comment extends React.Component {
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
+ queryData={queryData}
/>
{ isActive && (currentUser && (comment.user.id === currentUser.id)) &&
@@ -457,6 +466,7 @@ export default class Comment extends React.Component {
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
+ queryData={queryData}
/>
}
@@ -468,6 +478,7 @@ export default class Comment extends React.Component {
{!disableReply &&
@@ -484,6 +495,7 @@ export default class Comment extends React.Component {
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
+ queryData={queryData}
inline
/>
@@ -509,9 +521,7 @@ export default class Comment extends React.Component {
{activeReplyBox === comment.id
? {
- setActiveReplyBox('');
- }}
+ commentPostedHandler={this.commentPostedHandler}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index aa1221943..cf74813e9 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -40,6 +40,15 @@ class Stream extends React.Component {
}
}
+ commentIsIgnored = (comment) => {
+ const me = this.props.root.me;
+ return (
+ me &&
+ me.ignoredUsers &&
+ me.ignoredUsers.find((u) => u.id === comment.user.id)
+ );
+ };
+
render() {
const {
data,
@@ -48,7 +57,7 @@ class Stream extends React.Component {
setActiveReplyBox,
appendItemArray,
commentClassNames,
- root: {asset, asset: {comment, comments, totalCommentCount}, me},
+ root: {asset, asset: {comment, comments, totalCommentCount}},
postComment,
addNotification,
editComment,
@@ -89,16 +98,9 @@ class Stream extends React.Component {
user.suspension.until &&
new Date(user.suspension.until) > new Date();
- const commentIsIgnored = (comment) => {
- return (
- me &&
- me.ignoredUsers &&
- me.ignoredUsers.find((u) => u.id === comment.user.id)
- );
- };
-
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
- const slotProps = {data, root, asset};
+ const slotProps = {data};
+ const slotQueryData = {root, asset};
if (!comment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
@@ -160,6 +162,7 @@ class Stream extends React.Component {
@@ -194,7 +197,7 @@ class Stream extends React.Component {
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={highlightedComment.id}
- commentIsIgnored={commentIsIgnored}
+ commentIsIgnored={this.commentIsIgnored}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
@@ -209,6 +212,7 @@ class Stream extends React.Component {
>
@@ -219,6 +223,7 @@ class Stream extends React.Component {
tabSlot={'streamTabs'}
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
+ queryData={slotQueryData}
appendTabs={
All Comments {totalCommentCount}
@@ -245,7 +250,7 @@ class Stream extends React.Component {
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
- commentIsIgnored={commentIsIgnored}
+ commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js
index 40c2fa000..5f3ee0c9d 100644
--- a/client/coral-embed-stream/src/containers/Comment.js
+++ b/client/coral-embed-stream/src/containers/Comment.js
@@ -3,7 +3,9 @@ import React from 'react';
import Comment from '../components/Comment';
import {withFragments} from 'coral-framework/hocs';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
+import {THREADING_LEVEL} from '../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
+import {nest} from '../graphql/utils';
const slots = [
'streamQuestionArea',
@@ -48,6 +50,36 @@ const withAnimateEnter = hoistStatics((BaseComponent) => {
return WithAnimateEnter;
});
+const singleCommentFragment = gql`
+ fragment CoralEmbedStream_Comment_SingleComment on Comment {
+ id
+ body
+ created_at
+ status
+ replyCount
+ tags {
+ tag {
+ name
+ }
+ }
+ user {
+ id
+ username
+ }
+ action_summaries {
+ __typename
+ count
+ current_user {
+ id
+ }
+ }
+ editing {
+ edited
+ editableUntil
+ }
+ }
+`;
+
const withCommentFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
@@ -63,33 +95,21 @@ const withCommentFragments = withFragments({
`,
comment: gql`
fragment CoralEmbedStream_Comment_comment on Comment {
- id
- body
- created_at
- status
- replyCount
- tags {
- tag {
- name
+ ...CoralEmbedStream_Comment_SingleComment
+ ${nest(`
+ replies(limit: 3, excludeIgnored: $excludeIgnored) {
+ nodes {
+ ...CoralEmbedStream_Comment_SingleComment
+ ...nest
+ }
+ hasNextPage
+ startCursor
+ endCursor
}
- }
- user {
- id
- username
- }
- action_summaries {
- __typename
- count
- current_user {
- id
- }
- }
- editing {
- edited
- editableUntil
- }
+ `, THREADING_LEVEL)}
${getSlotFragmentSpreads(slots, 'comment')}
}
+ ${singleCommentFragment}
`
});
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index acc178261..f433bfb77 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -161,19 +161,11 @@ class StreamContainer extends React.Component {
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
+ status
+ user {
+ id
+ }
...${getDefinitionName(Comment.fragments.comment)}
- ${nest(`
- replies(excludeIgnored: $excludeIgnored) {
- nodes {
- id
- ...${getDefinitionName(Comment.fragments.comment)}
- ...nest
- }
- hasNextPage
- startCursor
- endCursor
- }
- `, THREADING_LEVEL)}
}
${Comment.fragments.comment}
`;
@@ -210,27 +202,14 @@ const LOAD_MORE_QUERY = gql`
query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
nodes {
- id
- ...${getDefinitionName(Comment.fragments.comment)}
- ${nest(`
- replies(limit: 3, excludeIgnored: $excludeIgnored) {
- nodes {
- id
- ...${getDefinitionName(Comment.fragments.comment)}
- ...nest
- }
- hasNextPage
- startCursor
- endCursor
- }
- `, THREADING_LEVEL)}
+ ...CoralEmbedStream_Stream_comment
}
hasNextPage
startCursor
endCursor
}
}
- ${Comment.fragments.comment}
+ ${commentFragment}
`;
const slots = [
diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js
index 4a313d2b4..7b91d58ee 100644
--- a/client/coral-embed-stream/src/containers/StreamTabPanel.js
+++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js
@@ -2,7 +2,7 @@ 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 {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins';
import {Tab, TabPane} from 'coral-ui';
import {getShallowChanges} from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
@@ -43,14 +43,14 @@ class StreamTabPanelContainer extends React.Component {
}
getSlotComponents(slot, props = this.props) {
- return getSlotComponents(slot, props.reduxState, props.slotProps);
+ return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
}
getPluginTabElements(props = this.props) {
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => (
@@ -61,7 +61,7 @@ class StreamTabPanelContainer extends React.Component {
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => (
));
@@ -95,6 +95,8 @@ StreamTabPanelContainer.propTypes = {
fallbackTab: PropTypes.string.isRequired,
tabSlot: PropTypes.string.isRequired,
tabPaneSlot: PropTypes.string.isRequired,
+ slotProps: PropTypes.object.isRequired,
+ queryData: PropTypes.object,
className: PropTypes.string,
sub: PropTypes.bool,
};
diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js
index 0b0f4d325..838382978 100644
--- a/client/coral-framework/components/Slot.js
+++ b/client/coral-framework/components/Slot.js
@@ -2,11 +2,13 @@ import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
-import {getSlotElements} from 'coral-framework/helpers/plugins';
+import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import {getShallowChanges} from 'coral-framework/utils';
+const emptyConfig = {};
+
class Slot extends React.Component {
shouldComponentUpdate(next) {
@@ -23,20 +25,20 @@ class Slot extends React.Component {
return changes.length !== 0;
}
- getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, ...rest} = this.props) {
+ getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) {
return rest;
}
getChildren(props = this.props) {
- return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props));
+ return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData);
}
render() {
- const {inline = false, className, reduxState, defaultComponent: DefaultComponent} = this.props;
+ const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
let children = this.getChildren();
- const pluginConfig = reduxState.config.pluginConfig || {};
+ const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
if (children.length === 0 && DefaultComponent) {
- children = ;
+ children = ;
}
return (
@@ -48,7 +50,8 @@ class Slot extends React.Component {
}
Slot.propTypes = {
- fill: React.PropTypes.string.isRequired
+ fill: React.PropTypes.string.isRequired,
+ queryData: React.PropTypes.object,
};
const mapStateToProps = (state) => ({
diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js
index 7bebaa1f6..7f6e5f1c4 100644
--- a/client/coral-framework/helpers/plugins.js
+++ b/client/coral-framework/helpers/plugins.js
@@ -11,8 +11,11 @@ import camelize from './camelize';
import plugins from 'pluginsConfig';
import uuid from 'uuid/v4';
-export function getSlotComponents(slot, reduxState, props = {}) {
- const pluginConfig = reduxState.config.plugin_config || {};
+// This is returned for pluginConfig when it is empty.
+const emptyConfig = {};
+
+export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) {
+ const pluginConfig = reduxState.config.plugin_config || emptyConfig;
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
@@ -25,7 +28,7 @@ export function getSlotComponents(slot, reduxState, props = {}) {
if(!component.isExcluded) {
return true;
}
- let resolvedProps = {...props, config: pluginConfig};
+ let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData);
if (component.mapStateToProps) {
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
}
@@ -33,17 +36,35 @@ export function getSlotComponents(slot, reduxState, props = {}) {
});
}
-export function isSlotEmpty(slot, reduxState, props) {
- return getSlotComponents(slot, reduxState, props).length === 0;
+export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
+ return getSlotComponents(slot, reduxState, props, queryData).length === 0;
+}
+
+/**
+ * getSlotComponentProps calculate the props we would pass to the slot component.
+ * query datas are only passed to the component if it is defined in `component.fragments`.
+ */
+export function getSlotComponentProps(component, reduxState, props, queryData) {
+ const pluginConfig = reduxState.config.plugin_config || emptyConfig;
+ return {
+ ...props,
+ config: pluginConfig,
+ ...(
+ component.fragments
+ ? pick(queryData, Object.keys(component.fragments))
+ : queryData // TODO: should be {}
+ )
+ };
}
/**
* Returns React Elements for given slot.
*/
-export function getSlotElements(slot, reduxState, props = {}) {
- const pluginConfig = reduxState.config.plugin_config || {};
- return getSlotComponents(slot, reduxState, props)
- .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
+export function getSlotElements(slot, reduxState, props = {}, queryData = {}) {
+ return getSlotComponents(slot, reduxState, props, queryData)
+ .map((component, i) => {
+ return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)});
+ });
}
export function getSlotFragments(slot, part) {
diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js
index c422f8705..aef7147d8 100644
--- a/client/coral-framework/hocs/withFragments.js
+++ b/client/coral-framework/hocs/withFragments.js
@@ -1,17 +1,98 @@
-// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
-
import React from 'react';
+import graphql from 'graphql-anywhere';
import {resolveFragments} from 'coral-framework/services/graphqlRegistry';
import mapValues from 'lodash/mapValues';
import hoistStatics from 'recompose/hoistStatics';
+import {getShallowChanges} from 'coral-framework/utils';
+
+// TODO: Should not depend on `props.data`
+// Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38
+function filter(doc, data, variables) {
+ const resolver = (
+ fieldName,
+ root,
+ args,
+ context,
+ info,
+ ) => {
+ return root[info.resultKey];
+ };
+
+ return graphql(resolver, doc, data, null, variables);
+}
+
+// filterProps returns only the property as defined in the fragments.
+// TODO: Should not depend on `props.data`
+function filterProps(props, fragments) {
+ const filtered = {};
+ Object.keys(fragments).forEach((key) => {
+ if (!(key in props)) {
+ return;
+ }
+ filtered[key] = filter(fragments[key], props[key], props.data.variables);
+ });
+ return filtered;
+}
+
+// hasEqualLeaves compares two different apollo query result for equality.
+function hasEqualLeaves(a, b, path = '') {
+ for (const key in a) {
+ if (typeof a[key] === 'object') {
+ if (Array.isArray(a[key])) {
+ if (a[key].length !== b[key].length) {
+ return false;
+ }
+ }
+ if (!hasEqualLeaves(a[key], b[key], `${path}.${key}`)) {
+ return false;
+ }
+ continue;
+ }
+ if (a[key] !== b[key]) {
+ return false;
+ }
+ }
+ return true;
+}
export default (fragments) => hoistStatics((BaseComponent) => {
class WithFragments extends React.Component {
fragments = mapValues(fragments, (val) => resolveFragments(val));
+ fragmentKeys = Object.keys(fragments).sort();
+
+ // Cache variables between lifecycles to speed up render.
+ filteredProps = filterProps(this.props, this.fragments)
+ queryDataHasChanged = false;
+ lastFilteredProps = null;
+ shallowChanges = null;
+
+ componentWillReceiveProps(next) {
+ this.shallowChanges = getShallowChanges(this.props, next);
+ this.queryDataHasChanged = this.fragmentKeys.some((key) => this.shallowChanges.indexOf(key) >= 0);
+
+ if (this.queryDataHasChanged) {
+
+ // If query data has changed, we compute the next filtered props.
+ this.lastFilteredProps = this.filteredProps;
+ this.filteredProps = filterProps(next, this.fragments);
+ }
+ }
+
+ shouldComponentUpdate(next) {
+
+ // If only query data was changed.
+ if (this.queryDataHasChanged && this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0)) {
+ return !hasEqualLeaves(this.lastFilteredProps, this.filteredProps);
+ }
+
+ return this.shallowChanges.length !== 0;
+ }
render() {
+ const queryProps = this.filteredProps;
return ;
}
}
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
index 9ac49cf43..0d118a12f 100644
--- a/plugin-api/beta/client/hocs/withReaction.js
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -176,7 +176,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
createdSubscription = context.client.subscribe({
query: REACTION_CREATED_SUBSCRIPTION,
variables: {
- assetId: this.props.root.asset.id,
+ assetId: this.props.asset.id,
},
}).subscribe({
next: this.onReactionCreated,
@@ -186,7 +186,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
deletedSubscription = context.client.subscribe({
query: REACTION_DELETED_SUBSCRIPTION,
variables: {
- assetId: this.props.root.asset.id,
+ assetId: this.props.asset.id,
},
}).subscribe({
next: this.onReactionDeleted,
@@ -372,9 +372,16 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
const enhance = compose(
withFragments({
+ asset: gql`
+ fragment ${Reaction}Button_asset on Asset {
+ id
+ }
+ `,
comment: gql`
fragment ${Reaction}Button_comment on Comment {
+ id
action_summaries {
+ __typename
... on ${Reaction}ActionSummary {
count
current_user {
diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js
index 428c3f43f..b1d565557 100644
--- a/plugin-api/beta/client/hocs/withTags.js
+++ b/plugin-api/beta/client/hocs/withTags.js
@@ -93,8 +93,14 @@ export default (tag) => hoistStatics((WrappedComponent) => {
const enhance = compose(
withFragments({
+ asset: gql`
+ fragment ${Tag}Button_asset on Asset {
+ id
+ }
+ `,
comment: gql`
fragment ${Tag}Button_comment on Comment {
+ id
tags {
tag {
name
diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
index 2195b0107..63ce30d01 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
@@ -16,8 +16,8 @@ class TabPaneContainer extends React.Component {
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
- cursor: this.props.root.asset.featuredComments.endCursor,
- asset_id: this.props.root.asset.id,
+ cursor: this.props.asset.featuredComments.endCursor,
+ asset_id: this.props.asset.id,
sort: 'REVERSE_CHRONOLOGICAL',
excludeIgnored: this.props.data.variables.excludeIgnored,
},