diff --git a/client/coral-admin/src/components/LoadMore.css b/client/coral-admin/src/components/LoadMore.css new file mode 100644 index 000000000..64c51f870 --- /dev/null +++ b/client/coral-admin/src/components/LoadMore.css @@ -0,0 +1,21 @@ +.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; +} + + diff --git a/client/coral-admin/src/routes/Moderation/components/LoadMore.js b/client/coral-admin/src/components/LoadMore.js similarity index 63% rename from client/coral-admin/src/routes/Moderation/components/LoadMore.js rename to client/coral-admin/src/components/LoadMore.js index 612629647..969c6734f 100644 --- a/client/coral-admin/src/routes/Moderation/components/LoadMore.js +++ b/client/coral-admin/src/components/LoadMore.js @@ -1,9 +1,10 @@ import React, {PropTypes} from 'react'; import {Button} from 'coral-ui'; -import styles from './styles.css'; +import styles from './LoadMore.css'; +import cn from 'classnames'; -const LoadMore = ({loadMore, showLoadMore}) => -
+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 ccc762641..72c5e4d63 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; @@ -499,4 +479,4 @@ span { right: 0px; top: 0px; text-align: right; -} \ No newline at end of file +} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index a8bba1ca3..6c231f4ae 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -401,13 +401,13 @@ export default class Comment extends React.Component {
-
+
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-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/plugins/talk-plugin-featured-comments/client/components/TabPane.js b/plugins/talk-plugin-featured-comments/client/components/TabPane.js index 3cf8b2cfd..2f5d0c7c2 100644 --- a/plugins/talk-plugin-featured-comments/client/components/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/components/TabPane.js @@ -36,7 +36,7 @@ class TabPane extends React.Component { {featuredComments.hasNextPage && }