Unverified Commit 7eb583a8 authored by Karishma Chadha's avatar Karishma Chadha Committed by GitHub

Merge pull request #5217 from kchadha/studio-comments-fetching

Studio Comments Fetching & Initial UI
parents 23027f0b 6026eded
const eachLimit = require('async/eachLimit');
const api = require('../lib/api');
const log = require('../lib/log');
const COMMENT_LIMIT = 20;
const {
addNewComment,
resetComments,
Status,
setFetchStatus,
setCommentDeleted,
setCommentReported,
setCommentRestored,
setMoreCommentsToLoad,
setComments,
setError,
setReplies,
setRepliesDeleted,
setRepliesRestored
} = require('../redux/comments.js');
const getReplies = (studioId, commentIds, offset, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('replies', Status.FETCHING));
const fetchedReplies = {};
eachLimit(commentIds, 10, (parentId, callback) => {
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${parentId}/replies`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
return callback(`Error fetching comment replies: ${err}`);
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
return callback('No comment reply information');
}
fetchedReplies[parentId] = body;
callback(null, body);
});
}, err => {
if (err) {
dispatch(setFetchStatus('replies', Status.ERROR));
dispatch(setError(err));
return;
}
dispatch(setFetchStatus('replies', Status.FETCHED));
dispatch(setReplies(fetchedReplies));
});
});
const getTopLevelComments = (id, offset, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('comments', Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${id}/comments`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments(body));
dispatch(getReplies(id, body.map(comment => comment.id), 0, isAdmin, token));
// If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
// any more server query complexity, so seems worth it. In the case of a project with
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
// clickable, but upon clicking it will go away.
dispatch(setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
});
});
const getCommentById = (studioId, commentId, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('comments', Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${commentId}`,
authentication: token ? token : null
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (!body || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
if (body.parent_id) {
// If the comment is a reply, load the parent
return dispatch(getCommentById(studioId, body.parent_id, isAdmin, token));
}
// If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body]));
dispatch(getReplies(studioId, [body.id], 0, isAdmin, token));
});
});
const deleteComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */
api({
uri: `/proxy/comments/studio/${studioId}/comment/${commentId}`,
authentication: token,
withCredentials: true,
method: 'DELETE',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentDeleted(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesDeleted(commentId));
}
});
});
const reportComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/studio/${studioId}/comment/${commentId}/report`,
authentication: token,
withCredentials: true,
method: 'POST',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
// TODO use the reportId in the response for unreporting functionality
dispatch(setCommentReported(commentId, topLevelCommentId));
});
});
const restoreComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/admin/studio/${studioId}/comment/${commentId}/undelete`,
authentication: token,
withCredentials: true,
method: 'PUT',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentRestored(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesRestored(commentId));
}
});
});
module.exports = {
getTopLevelComments,
getCommentById,
getReplies,
deleteComment,
reportComment,
restoreComment,
// Re-export these specific action creators directly so the implementer
// does not need to go to two places for comment actions
addNewComment,
resetComments
};
...@@ -110,7 +110,7 @@ class Comment extends React.Component { ...@@ -110,7 +110,7 @@ class Comment extends React.Component {
highlighted, highlighted,
id, id,
parentId, parentId,
projectId, postURI,
replyUsername, replyUsername,
visibility visibility
} = this.props; } = this.props;
...@@ -234,7 +234,7 @@ class Comment extends React.Component { ...@@ -234,7 +234,7 @@ class Comment extends React.Component {
isReply isReply
commenteeId={author.id} commenteeId={author.id}
parentId={parentId || id} parentId={parentId || id}
projectId={projectId} postURI={postURI}
onAddComment={this.handlePostReply} onAddComment={this.handlePostReply}
onCancel={this.handleToggleReplying} onCancel={this.handleToggleReplying}
/> />
...@@ -285,7 +285,7 @@ Comment.propTypes = { ...@@ -285,7 +285,7 @@ Comment.propTypes = {
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func, onRestore: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
replyUsername: PropTypes.string, replyUsername: PropTypes.string,
visibility: PropTypes.string visibility: PropTypes.string
}; };
......
...@@ -79,7 +79,7 @@ class ComposeComment extends React.Component { ...@@ -79,7 +79,7 @@ class ComposeComment extends React.Component {
handlePost () { handlePost () {
this.setState({status: ComposeStatus.SUBMITTING}); this.setState({status: ComposeStatus.SUBMITTING});
api({ api({
uri: `/proxy/comments/project/${this.props.projectId}`, uri: this.props.postURI,
authentication: this.props.user.token, authentication: this.props.user.token,
withCredentials: true, withCredentials: true,
method: 'POST', method: 'POST',
...@@ -434,7 +434,7 @@ ComposeComment.propTypes = { ...@@ -434,7 +434,7 @@ ComposeComment.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
username: PropTypes.string, username: PropTypes.string,
......
...@@ -87,7 +87,7 @@ class TopLevelComment extends React.Component { ...@@ -87,7 +87,7 @@ class TopLevelComment extends React.Component {
onReport, onReport,
onRestore, onRestore,
replies, replies,
projectId, postURI,
visibility visibility
} = this.props; } = this.props;
...@@ -97,7 +97,7 @@ class TopLevelComment extends React.Component { ...@@ -97,7 +97,7 @@ class TopLevelComment extends React.Component {
<FlexRow className="comment-container"> <FlexRow className="comment-container">
<Comment <Comment
highlighted={highlightedCommentId === id} highlighted={highlightedCommentId === id}
projectId={projectId} postURI={postURI}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
{...{ {...{
author, author,
...@@ -138,7 +138,7 @@ class TopLevelComment extends React.Component { ...@@ -138,7 +138,7 @@ class TopLevelComment extends React.Component {
id={reply.id} id={reply.id}
key={reply.id} key={reply.id}
parentId={id} parentId={id}
projectId={projectId} postURI={postURI}
replyUsername={this.authorUsername(reply.commentee_id)} replyUsername={this.authorUsername(reply.commentee_id)}
visibility={reply.visibility} visibility={reply.visibility}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
...@@ -188,7 +188,7 @@ TopLevelComment.propTypes = { ...@@ -188,7 +188,7 @@ TopLevelComment.propTypes = {
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func, onRestore: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.arrayOf(PropTypes.object),
visibility: PropTypes.string visibility: PropTypes.string
}; };
......
...@@ -581,7 +581,7 @@ const PreviewPresentation = ({ ...@@ -581,7 +581,7 @@ const PreviewPresentation = ({
{projectInfo.comments_allowed ? ( {projectInfo.comments_allowed ? (
isLoggedIn ? ( isLoggedIn ? (
isShared && <ComposeComment isShared && <ComposeComment
projectId={projectId} postURI={`/proxy/comments/project/${projectId}`}
onAddComment={onAddComment} onAddComment={onAddComment}
/> />
) : ( ) : (
...@@ -613,7 +613,7 @@ const PreviewPresentation = ({ ...@@ -613,7 +613,7 @@ const PreviewPresentation = ({
key={comment.id} key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad} moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id} parentId={comment.parent_id}
projectId={projectId} postURI={`/proxy/comments/project/${projectId}`}
replies={replies && replies[comment.id] ? replies[comment.id] : []} replies={replies && replies[comment.id] ? replies[comment.id] : []}
visibility={comment.visibility} visibility={comment.visibility}
onAddComment={onAddComment} onAddComment={onAddComment}
......
import React from 'react'; import React, {useEffect, useCallback} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom'; import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
const StudioComments = () => { import Button from '../../components/forms/button.jsx';
import ComposeComment from '../preview/comment/compose-comment.jsx';
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
const StudioComments = ({
comments,
getTopLevelComments,
handleNewComment,
moreCommentsToLoad,
replies,
shouldShowCommentComposer
}) => {
const {studioId} = useParams(); const {studioId} = useParams();
const handleLoadComments = useCallback(() => {
getTopLevelComments(studioId, comments.length);
}, [studioId, comments.length]);
useEffect(() => {
if (comments.length === 0) getTopLevelComments(studioId, 0);
}, [studioId]);
return ( return (
<div> <div>
<h2>Comments</h2> <h2>Comments</h2>
<p>Studio {studioId}</p> <div>
{shouldShowCommentComposer &&
<ComposeComment
postURI={`/proxy/comments/studio/${studioId}`}
onAddComment={handleNewComment}
/>
}
{comments.map(comment => (
<TopLevelComment
author={comment.author}
canReply={shouldShowCommentComposer}
content={comment.content}
datetimeCreated={comment.datetime_created}
id={comment.id}
key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id}
postURI={`/proxy/comments/studio/${studioId}`}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
visibility={comment.visibility}
/>
))}
{moreCommentsToLoad &&
<Button
className="button load-more-button"
onClick={handleLoadComments}
>
<FormattedMessage id="general.loadMore" />
</Button>
}
</div>
</div> </div>
); );
}; };
export default StudioComments; StudioComments.propTypes = {
comments: PropTypes.arrayOf(PropTypes.shape({})),
getTopLevelComments: PropTypes.func,
handleNewComment: PropTypes.func,
moreCommentsToLoad: PropTypes.bool,
replies: PropTypes.shape({}),
shouldShowCommentComposer: PropTypes.bool
};
export default connect(
state => ({
comments: state.comments.comments,
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,
// TODO permissions like this to a selector for testing
shouldShowCommentComposer: !!state.session.session.user // is logged in
}),
{
getTopLevelComments: studioCommentActions.getTopLevelComments,
handleNewComment: studioCommentActions.addNewComment
}
)(StudioComments);
...@@ -25,6 +25,7 @@ import { ...@@ -25,6 +25,7 @@ import {
} from './lib/redux-modules'; } from './lib/redux-modules';
const {studioReducer} = require('../../redux/studio'); const {studioReducer} = require('../../redux/studio');
const {commentsReducer} = require('../../redux/comments');
const StudioShell = () => { const StudioShell = () => {
const match = useRouteMatch(); const match = useRouteMatch();
...@@ -75,6 +76,7 @@ render( ...@@ -75,6 +76,7 @@ render(
[curators.key]: curators.reducer, [curators.key]: curators.reducer,
[managers.key]: managers.reducer, [managers.key]: managers.reducer,
[activity.key]: activity.reducer, [activity.key]: activity.reducer,
studio: studioReducer studio: studioReducer,
comments: commentsReducer
} }
); );
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment