From 8ef9a7e5e4a12fce9e6a9ae39876564d97157fa2 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 18 Feb 2019 16:35:21 -0600 Subject: [PATCH] comment reporting for moderation --- backend/grant/proposal/views.py | 35 +++++++++-------- frontend/client/components/Comment/index.tsx | 23 +++++++++-- .../components/Proposal/Comments/index.tsx | 8 +--- frontend/client/modules/proposals/actions.ts | 23 +++++++++-- frontend/client/modules/proposals/reducers.ts | 39 +++++++++++++++++++ 5 files changed, 97 insertions(+), 31 deletions(-) diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index ec647c63..dcc8de77 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -79,6 +79,24 @@ def get_proposal_comments(proposal_id, page, filters, search, sort): return page +@blueprint.route("//comments//report", methods=["PUT"]) +@requires_email_verified_auth +@endpoint.api() +def report_proposal_comment(proposal_id, comment_id): + # Make sure proposal exists + proposal = Proposal.query.filter_by(id=proposal_id).first() + if not proposal: + return {"message": "No proposal matching id"}, 404 + + comment = Comment.query.filter_by(id=comment_id).first() + if not comment: + return {"message": "Comment doesn’t exist"}, 404 + + comment.report(True) + db.session.commit() + return None, 200 + + @blueprint.route("//comments", methods=["POST"]) @requires_email_verified_auth @endpoint.api( @@ -138,23 +156,6 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id): return dumped_comment, 201 -@blueprint.route("//comments//report", methods=["PUT"]) -@requires_email_verified_auth -@endpoint.api -def report_proposal_comment(proposal_id, comment_id): - # Make sure proposal exists - proposal = Proposal.query.filter_by(id=proposal_id).first() - if not proposal: - return {"message": "No proposal matching id"}, 404 - - comment = Comment.query.filter_by(id=comment_id).first() - if not comment: - return {"message": "Comment doesn’t exist"}, 404 - - comment.report(True) - return None, 200 - - @blueprint.route("/", methods=["GET"]) @endpoint.api( parameter('page', type=int, required=False), diff --git a/frontend/client/components/Comment/index.tsx b/frontend/client/components/Comment/index.tsx index f22eb213..d2d45587 100644 --- a/frontend/client/components/Comment/index.tsx +++ b/frontend/client/components/Comment/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { connect } from 'react-redux'; import moment from 'moment'; -import { Button } from 'antd'; +import { Button, message } from 'antd'; import { Link } from 'react-router-dom'; import Markdown from 'components/Markdown'; import UserAvatar from 'components/UserAvatar'; import MarkdownEditor, { MARKDOWN_TYPE } from 'components/MarkdownEditor'; -import { postProposalComment } from 'modules/proposals/actions'; +import { postProposalComment, reportProposalComment } from 'modules/proposals/actions'; import { getIsSignedIn } from 'modules/auth/selectors'; import { Comment as IComment } from 'types'; import { AppState } from 'store/reducers'; @@ -24,6 +24,7 @@ interface StateProps { interface DispatchProps { postProposalComment: typeof postProposalComment; + reportProposalComment: typeof reportProposalComment; } type Props = OwnProps & StateProps & DispatchProps; @@ -76,7 +77,12 @@ class Comment extends React.Component { {isReplying ? 'Cancel' : 'Reply'} - {/*Report*/} + {!comment.hidden && + !comment.reported && ( + + Report + + )} )} @@ -120,6 +126,16 @@ class Comment extends React.Component { const { reply } = this.state; this.props.postProposalComment(comment.proposalId, reply, comment.id); }; + + private report = async () => { + const { proposalId, id } = this.props.comment; + const res = await this.props.reportProposalComment(proposalId, id); + if ((res as any).error) { + message.error('Problem reporting comment: ' + (res as any).payload); + } else { + message.success('Comment reported'); + } + }; } const ConnectedComment = connect( @@ -130,6 +146,7 @@ const ConnectedComment = connect( }), { postProposalComment, + reportProposalComment, }, )(Comment); diff --git a/frontend/client/components/Proposal/Comments/index.tsx b/frontend/client/components/Proposal/Comments/index.tsx index 66bf923a..d5e1a300 100644 --- a/frontend/client/components/Proposal/Comments/index.tsx +++ b/frontend/client/components/Proposal/Comments/index.tsx @@ -3,11 +3,7 @@ import { connect } from 'react-redux'; import { Button, message, Skeleton, Alert } from 'antd'; import { AppState } from 'store/reducers'; import { Proposal } from 'types'; -import { - fetchProposalComments, - postProposalComment, - reportProposalComment, -} from 'modules/proposals/actions'; +import { fetchProposalComments, postProposalComment } from 'modules/proposals/actions'; import { getIsVerified, getIsSignedIn } from 'modules/auth/selectors'; import Comments from 'components/Comments'; import Placeholder from 'components/Placeholder'; @@ -29,7 +25,6 @@ interface StateProps { interface DispatchProps { fetchProposalComments: typeof fetchProposalComments; postProposalComment: typeof postProposalComment; - reportProposalComment: typeof reportProposalComment; } type Props = DispatchProps & OwnProps & StateProps; @@ -182,6 +177,5 @@ export default connect( { fetchProposalComments, postProposalComment, - reportProposalComment, }, )(ProposalComments); diff --git a/frontend/client/modules/proposals/actions.ts b/frontend/client/modules/proposals/actions.ts index 0e39d0bd..47cdd719 100644 --- a/frontend/client/modules/proposals/actions.ts +++ b/frontend/client/modules/proposals/actions.ts @@ -4,6 +4,7 @@ import { getProposal, getProposalComments, getProposalUpdates, + reportProposalComment as apiReportProposalComment, getProposalContributions, postProposalComment as apiPostProposalComment, requestProposalPayout, @@ -209,9 +210,23 @@ export function reportProposalComment( proposalId: Proposal['proposalId'], commentId: Comment['id'], ) { - return (dispatch: Dispatch) => { - // dispatch({ - // type: types.POST_PROPOSAL_COMMENT, - // }) + return async (dispatch: Dispatch) => { + dispatch({ type: types.REPORT_PROPOSAL_COMMENT_PENDING, payload: { commentId } }); + + try { + await apiReportProposalComment(proposalId, commentId); + return dispatch({ + type: types.REPORT_PROPOSAL_COMMENT_FULFILLED, + payload: { + commentId, + }, + }); + } catch (err) { + return dispatch({ + type: types.REPORT_PROPOSAL_COMMENT_REJECTED, + payload: err.message || err.toString(), + error: true, + }); + } }; } diff --git a/frontend/client/modules/proposals/reducers.ts b/frontend/client/modules/proposals/reducers.ts index 709522c7..1594994c 100644 --- a/frontend/client/modules/proposals/reducers.ts +++ b/frontend/client/modules/proposals/reducers.ts @@ -172,6 +172,42 @@ function addPostedComment(state: ProposalState, payload: PostCommentPayload) { }; } +function updateCommentInStore( + state: ProposalState, + commentId: Comment['id'], + update: Partial, +) { + // clone so we can mutate with great abandon! + const pages = cloneDeep(state.detailComments.pages); + // recursive populate replies for nested comment + const f = (id: number, p: Comment) => { + if (p.id === id) { + Object.entries(update).forEach(([k, v]) => { + (p as any)[k] = v; + }); + return; + } else { + p.replies.forEach(x => f(id, x)); + } + }; + // pages > page > comments + pages.forEach(p => + p.forEach(c => { + f(commentId, c); + }), + ); + + return { + ...state, + isPostCommentPending: false, + detailComments: { + ...state.detailComments, + pages, + total: state.detailComments.total + 1, + }, + }; +} + export default (state = INITIAL_STATE, action: any) => { const { payload } = action; switch (action.type) { @@ -336,6 +372,9 @@ export default (state = INITIAL_STATE, action: any) => { }, }; + case types.REPORT_PROPOSAL_COMMENT_FULFILLED: + return updateCommentInStore(state, payload.commentId, { reported: true }); + case types.PROPOSAL_UPDATES_PENDING: return { ...state,