+const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
+
{
showLoadMore &&
+
);
diff --git a/client/coral-admin/src/components/UserDetailComment.css b/client/coral-admin/src/components/UserDetailComment.css
index 0e10f00c0..b042aea39 100644
--- a/client/coral-admin/src/components/UserDetailComment.css
+++ b/client/coral-admin/src/components/UserDetailComment.css
@@ -8,6 +8,10 @@
min-height: 0;
}
+.root:last-child {
+ border: 0;
+}
+
.rootSelected {
background-color: #ecf4ff;
}
diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js
index a54e70734..e59629a85 100644
--- a/client/coral-admin/src/containers/UserDetail.js
+++ b/client/coral-admin/src/containers/UserDetail.js
@@ -14,6 +14,7 @@ import {
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
+import update from 'immutability-helper';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
@@ -32,6 +33,7 @@ const slots = [
];
class UserDetailContainer extends React.Component {
+ isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
@@ -40,7 +42,6 @@ class UserDetailContainer extends React.Component {
});
Promise.all(changes).then(() => {
- this.props.data.refetch(); // some comments may have moved out of this tab
this.props.clearUserDetailSelections(); // un-select everything
});
}
@@ -61,12 +62,53 @@ class UserDetailContainer extends React.Component {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
+ loadMore = () => {
+ if (this.isLoadingMore) {
+ return;
+ }
+
+ this.isLoadingMore = true;
+ const variables = {
+ limit: 10,
+ cursor: this.props.root.comments.endCursor,
+ author_id: this.props.data.variables.author_id,
+ statuses: this.props.data.variables.statuses,
+ };
+ this.props.data.fetchMore({
+ query: LOAD_MORE_QUERY,
+ variables,
+ updateQuery: (prev, {fetchMoreResult:{comments}}) => {
+ return update(prev, {
+ comments: {
+ nodes: {$push: comments.nodes},
+ hasNextPage: {$set: comments.hasNextPage},
+ startCursor: {$set: comments.startCursor},
+ endCursor: {$set: comments.endCursor},
+ },
+ });
+ }
+ })
+ .then(() => {
+ this.isLoadingMore = false;
+ })
+ .catch((err) => {
+ this.isLoadingMore = false;
+ throw err;
+ });
+ };
+
+ componentWillReceiveProps(next) {
+ if (this.props.userId === null && next.userId) {
+ next.data.refetch();
+ }
+ }
+
render () {
if (!this.props.userId) {
return null;
}
- const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.userId;
+ const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0;
return
;
}
}
+const LOAD_MORE_QUERY = gql`
+ query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
+ comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
+ ...CoralAdmin_Moderation_CommentConnection
+ }
+ }
+ ${commentConnectionFragment}
+`;
+
export const withUserDetailQuery = withQuery(gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
index 57b551df2..7f96c7c90 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
@@ -4,7 +4,7 @@ import Comment from '../containers/Comment';
import styles from './styles.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
-import LoadMore from './LoadMore';
+import LoadMore from '../../../components/LoadMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css
index d1d1ae697..061e5351d 100644
--- a/client/coral-admin/src/routes/Moderation/components/styles.css
+++ b/client/coral-admin/src/routes/Moderation/components/styles.css
@@ -397,26 +397,6 @@ span {
}
}
-.loadMoreContainer {
- display: flex;
- justify-content: center;
- width: 100%;
-};
-
-.loadMore {
- width: 100%;
- text-align: center;
- color: #FFF;
- max-width: 660px;
- margin-bottom: 30px;
- background-color: #2376D8;
- cursor: pointer;
-}
-
-.loadMore:hover {
- background-color: #4399FF;
-}
-
.tabIcon {
position: relative;
top: 3px;
diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js
index 7166aa6e9..6c5d6bb23 100644
--- a/client/coral-embed-stream/src/components/AllCommentsPane.js
+++ b/client/coral-embed-stream/src/components/AllCommentsPane.js
@@ -93,6 +93,7 @@ class AllCommentsPane extends React.Component {
viewNewComments = () => {
this.setState(resetCursors);
+ this.props.emit('ui.AllCommentsPane.viewNewComments');
};
// getVisibileComments returns a list containing comments
@@ -142,6 +143,7 @@ class AllCommentsPane extends React.Component {
charCountEnable,
maxCharCount,
editComment,
+ emit,
} = this.props;
const {loadingState} = this.state;
@@ -181,6 +183,7 @@ class AllCommentsPane extends React.Component {
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
+ emit={emit}
/>;
})}
diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js
index 34f672d8a..6c231f4ae 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/components/Comment.js
@@ -224,6 +224,7 @@ export default class Comment extends React.Component {
return;
}
this.setState(resetCursors);
+ this.props.emit('ui.Comment.showMoreReplies');
};
showReplyBox = () => {
@@ -400,13 +401,13 @@ export default class Comment extends React.Component {
-
+
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index 1e0ff7ea9..7b9e5f566 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -292,6 +292,7 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
+ emit={this.props.emit}
/>
diff --git a/client/coral-embed-stream/src/components/Toggleable.js b/client/coral-embed-stream/src/components/Toggleable.js
index 1c70f2468..49f2a701b 100644
--- a/client/coral-embed-stream/src/components/Toggleable.js
+++ b/client/coral-embed-stream/src/components/Toggleable.js
@@ -1,10 +1,10 @@
import React from 'react';
import ClickOutside from 'coral-framework/components/ClickOutside';
import styles from './Toggleable.css';
-import classnames from 'classnames';
+import cn from 'classnames';
-const upArrow =
;
-const downArrow =
;
+const upArrow =
;
+const downArrow =
;
export default class Toggleable extends React.Component {
constructor(props) {
@@ -23,11 +23,11 @@ export default class Toggleable extends React.Component {
}
render() {
- const {children} = this.props;
+ const {children, className, ...rest} = this.props;
const {isOpen} = this.state;
return (
-
+
{isOpen ? children : null}
diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js
index 4cd544379..64acb2290 100644
--- a/client/coral-embed-stream/src/components/TopRightMenu.js
+++ b/client/coral-embed-stream/src/components/TopRightMenu.js
@@ -45,7 +45,7 @@ export class TopRightMenu extends React.Component {
}
};
return (
-
+
export default compose(
withFragments(fragments),
+ withEmit,
connect(mapStateToProps, mapDispatchToProps),
withPostComment,
withPostFlag,
diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js
index a16af23e7..f52cc4e7d 100644
--- a/client/coral-embed-stream/src/graphql/index.js
+++ b/client/coral-embed-stream/src/graphql/index.js
@@ -73,6 +73,11 @@ const extension = {
created_at
status
replyCount
+ asset {
+ id
+ title
+ url
+ }
tags {
tag {
name
@@ -190,6 +195,15 @@ const extension = {
}
return insertCommentIntoEmbedQuery(prev, comment);
},
+ CoralEmbedStream_Profile: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
+ return update(prev, {
+ me: {
+ comments: {
+ nodes: {$unshift: [comment]},
+ },
+ },
+ });
+ },
}
}),
EditComment: () => ({
diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js
index 1af2a9a59..5e5a8c0c0 100644
--- a/client/coral-framework/helpers/plugins.js
+++ b/client/coral-framework/helpers/plugins.js
@@ -10,7 +10,7 @@ import camelize from './camelize';
import plugins from 'pluginsConfig';
export function getSlotComponents(slot, reduxState, props = {}) {
- const pluginConfig = reduxState.config.pluginConfig || {};
+ const pluginConfig = reduxState.config.plugin_config || {};
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
@@ -39,7 +39,7 @@ export function isSlotEmpty(slot, reduxState, props) {
* Returns React Elements for given slot.
*/
export function getSlotElements(slot, reduxState, props = {}) {
- const pluginConfig = reduxState.config.pluginConfig || {};
+ const pluginConfig = reduxState.config.plugin_config || {};
return getSlotComponents(slot, reduxState, props)
.map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
}
diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js
index 91de923b2..0090847f7 100644
--- a/client/coral-settings/containers/ProfileContainer.js
+++ b/client/coral-settings/containers/ProfileContainer.js
@@ -1,7 +1,8 @@
import {connect} from 'react-redux';
-import {compose, graphql, gql} from 'react-apollo';
+import {compose, gql} from 'react-apollo';
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
+import {withQuery} from 'coral-framework/hocs';
import {withStopIgnoringUser} from 'coral-framework/graphql/mutations';
@@ -11,18 +12,12 @@ import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import CommentHistory from 'talk-plugin-history/CommentHistory';
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
+import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
+import update from 'immutability-helper';
import t from 'coral-framework/services/i18n';
class ProfileContainer extends Component {
- constructor() {
- super();
-
- this.state = {
- activeTab: 0
- };
- }
-
componentWillReceiveProps(nextProps) {
if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
@@ -31,21 +26,40 @@ class ProfileContainer extends Component {
}
}
- handleTabChange = (tab) => {
- this.setState({
- activeTab: tab
+ loadMore = () => {
+ return this.props.data.fetchMore({
+ query: LOAD_MORE_QUERY,
+ variables: {
+ limit: 5,
+ cursor: this.props.root.me.comments.endCursor,
+ },
+ updateQuery: (previous, {fetchMoreResult:{comments}}) => {
+ const updated = update(previous, {
+ me: {
+ comments: {
+ nodes: {
+ $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'),
+ },
+ hasNextPage: {$set: comments.hasNextPage},
+ endCursor: {$set: comments.endCursor},
+ },
+ }
+ });
+ return updated;
+ },
});
};
render() {
- const {auth, asset, data, showSignInDialog, stopIgnoringUser} = this.props;
- const {me} = this.props.data;
+ const {auth, asset, showSignInDialog, stopIgnoringUser} = this.props;
+ const {me} = this.props.root;
+ const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0;
if (!auth.loggedIn) {
return ;
}
- if (!me || data.loading) {
+ if (loading) {
return ;
}
@@ -73,14 +87,40 @@ class ProfileContainer extends Component {
{t('framework.my_comments')}
{me.comments.nodes.length
- ?
+ ?
: {t('user_no_comment')}
}
);
}
}
-const withQuery = graphql(
+const CommentFragment = gql`
+ fragment TalkSettings_CommentConnectionFragment on CommentConnection {
+ nodes {
+ id
+ body
+ asset {
+ id
+ title
+ url
+ }
+ created_at
+ }
+ endCursor
+ hasNextPage
+ }
+`;
+
+const LOAD_MORE_QUERY = gql`
+ query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) {
+ comments(query: {limit: $limit, cursor: $cursor}) {
+ ...TalkSettings_CommentConnectionFragment
+ }
+ }
+ ${CommentFragment}
+`;
+
+const withProfileQuery = withQuery(
gql`
query CoralEmbedStream_Profile {
me {
@@ -89,21 +129,13 @@ const withQuery = graphql(
id,
username,
}
- comments {
- nodes {
- id
- body
- asset {
- id
- title
- url
- }
- created_at
- }
+ comments(query: {limit: 10}) {
+ ...TalkSettings_CommentConnectionFragment
}
}
- }`
-);
+ }
+ ${CommentFragment}
+`);
const mapStateToProps = (state) => ({
user: state.user.toJS(),
@@ -117,5 +149,5 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withStopIgnoringUser,
- withQuery
+ withProfileQuery
)(ProfileContainer);
diff --git a/client/coral-ui/components/Drawer.css b/client/coral-ui/components/Drawer.css
index d6a7e6871..88c30425f 100644
--- a/client/coral-ui/components/Drawer.css
+++ b/client/coral-ui/components/Drawer.css
@@ -3,7 +3,7 @@
min-width: 550px;
position: fixed;
top: 0;
- right: -17px;
+ right: 0px;
bottom: 0;
background-color: white;
transition: transform 500ms ease-in-out;
diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js
index 72c4de982..d584d6a0a 100644
--- a/client/talk-plugin-history/CommentHistory.js
+++ b/client/talk-plugin-history/CommentHistory.js
@@ -1,25 +1,52 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import styles from './CommentHistory.css';
+import LoadMore from './LoadMore';
+import {forEachError} from 'plugin-api/beta/client/utils';
-const CommentHistory = (props) => {
- return (
-
-
- {props.comments.map((comment, i) => {
- return
;
- })}
+class CommentHistory extends React.Component {
+ state = {
+ loadingState: '',
+ };
+
+ loadMore = () => {
+ this.setState({loadingState: 'loading'});
+ this.props.loadMore()
+ .then(() => {
+ this.setState({loadingState: 'success'});
+ })
+ .catch((error) => {
+ this.setState({loadingState: 'error'});
+ forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
+ });
+ }
+
+ render() {
+ const {link, comments} = this.props;
+ return (
+
+
+ {comments.nodes.map((comment, i) => {
+ return ;
+ })}
+
+ {comments.hasNextPage &&
+
+ }
-
- );
-};
+ );
+ }
+}
CommentHistory.propTypes = {
- comments: PropTypes.array.isRequired
+ comments: PropTypes.object.isRequired
};
export default CommentHistory;
diff --git a/client/talk-plugin-history/LoadMore.js b/client/talk-plugin-history/LoadMore.js
new file mode 100644
index 000000000..a168ed43a
--- /dev/null
+++ b/client/talk-plugin-history/LoadMore.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Button} from 'coral-ui';
+import t from 'coral-framework/services/i18n';
+import cn from 'classnames';
+
+class LoadMore extends React.Component {
+ render () {
+ const {loadingState, loadMore} = this.props;
+ const disabled = loadingState === 'loading';
+ return (
+
+
+
+ );
+ }
+}
+
+LoadMore.propTypes = {
+ loadMore: PropTypes.func.isRequired,
+ loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']),
+};
+
+export default LoadMore;
diff --git a/docs/_docs/05-01-development-tools.md b/docs/_docs/05-01-development-tools.md
index 609d3ef48..fc19a077c 100644
--- a/docs/_docs/05-01-development-tools.md
+++ b/docs/_docs/05-01-development-tools.md
@@ -1,6 +1,6 @@
---
title: Development Tooling
-permalink: /docs/development/tools
+permalink: /docs/development/tools/
---
## Debugging
diff --git a/graph/helpers/response.js b/graph/helpers/response.js
index ebdbc02ec..1db1e9b89 100644
--- a/graph/helpers/response.js
+++ b/graph/helpers/response.js
@@ -2,18 +2,21 @@ const errors = require('../../errors');
const {Error: {ValidationError}} = require('mongoose');
/**
- * Wraps up a promise to return an object with the resolution of the promise
+ * Wraps up a promise or value to return an object with the resolution of the promise
* keyed at `key` or an error caught at `errors`.
*/
-const wrapResponse = (key) => (promise) => {
- return promise.then((value) => {
+const wrapResponse = (key) => async (promise) => {
+ try {
+ let value = await promise;
+
let res = {};
if (key) {
res[key] = value;
}
+
return res;
- }).catch((err) => {
+ } catch (err) {
if (err instanceof errors.APIError) {
return {
errors: [err]
@@ -25,7 +28,7 @@ const wrapResponse = (key) => (promise) => {
}
throw err;
- });
+ }
};
module.exports = wrapResponse;
diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js
index 2878d6561..5a851233a 100644
--- a/graph/resolvers/user.js
+++ b/graph/resolvers/user.js
@@ -29,12 +29,12 @@ const User = {
return null;
},
- comments({id}, _, {loaders: {Comments}, user}) {
+ comments({id}, {query}, {loaders: {Comments}, user}) {
// If the user is not an admin, only return comment list for the owner of
// the comments.
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === id)) {
- return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'});
+ return Comments.getByQuery(Object.assign({}, query, {author_id: id}));
}
return null;
diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index 68547692d..60b118522 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -3,3 +3,4 @@ export {default as withTags} from './withTags';
export {default as withFragments} from 'coral-framework/hocs/withFragments';
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
export {default as connect} from 'coral-framework/hocs/connect';
+export {default as withEmit} from 'coral-framework/hocs/withEmit';
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
index 40ee6e14c..7f43cc5b2 100644
--- a/plugin-api/beta/client/hocs/withReaction.js
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -271,6 +271,7 @@ export default (reaction) => (WrappedComponent) => {
alreadyReacted={alreadyReacted}
postReaction={this.postReaction}
deleteReaction={this.deleteReaction}
+ config={this.props.config}
/>;
}
}
diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js
index 75d434bef..e9b5bdff0 100644
--- a/plugin-api/beta/client/hocs/withTags.js
+++ b/plugin-api/beta/client/hocs/withTags.js
@@ -68,16 +68,17 @@ export default (tag) => (WrappedComponent) => {
}
render() {
- const {comment} = this.props;
+ const {comment, user, config} = this.props;
const alreadyTagged = isTagged(comment.tags, TAG);
return
;
}
}
diff --git a/plugins/talk-plugin-auth/client/components/SignInButton.js b/plugins/talk-plugin-auth/client/components/SignInButton.js
index 944796e88..0adb300a7 100644
--- a/plugins/talk-plugin-auth/client/components/SignInButton.js
+++ b/plugins/talk-plugin-auth/client/components/SignInButton.js
@@ -6,7 +6,7 @@ import {showSignInDialog} from 'coral-framework/actions/auth';
import t from 'coral-framework/services/i18n';
const SignInButton = ({loggedIn, showSignInDialog}) => (
-
+
{!loggedIn
?
diff --git a/services/actions.js b/services/actions.js
index c59b3a3f9..74c3bdad3 100644
--- a/services/actions.js
+++ b/services/actions.js
@@ -173,7 +173,8 @@ module.exports = class ActionsService {
return ActionModel.aggregate([
{$match},
{$group},
- {$project}
+ {$project},
+ {$sort: {action_type: 1, group_id: 1}},
]);
}