comment reporting for moderation

This commit is contained in:
Aaron 2019-02-18 16:35:21 -06:00
parent 1a87cadc4d
commit 8ef9a7e5e4
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
5 changed files with 97 additions and 31 deletions

View File

@ -79,6 +79,24 @@ def get_proposal_comments(proposal_id, page, filters, search, sort):
return page
@blueprint.route("/<proposal_id>/comments/<comment_id>/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 doesnt exist"}, 404
comment.report(True)
db.session.commit()
return None, 200
@blueprint.route("/<proposal_id>/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("/<proposal_id>/comments/<comment_id>/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 doesnt exist"}, 404
comment.report(True)
return None, 200
@blueprint.route("/", methods=["GET"])
@endpoint.api(
parameter('page', type=int, required=False),

View File

@ -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<Props> {
<a className="Comment-controls-button" onClick={this.toggleReply}>
{isReplying ? 'Cancel' : 'Reply'}
</a>
{/*<a className="Comment-controls-button">Report</a>*/}
{!comment.hidden &&
!comment.reported && (
<a className="Comment-controls-button" onClick={this.report}>
Report
</a>
)}
</div>
)}
@ -120,6 +126,16 @@ class Comment extends React.Component<Props> {
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<StateProps, DispatchProps, OwnProps, AppState>(
@ -130,6 +146,7 @@ const ConnectedComment = connect<StateProps, DispatchProps, OwnProps, AppState>(
}),
{
postProposalComment,
reportProposalComment,
},
)(Comment);

View File

@ -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<StateProps, DispatchProps, OwnProps, AppState>(
{
fetchProposalComments,
postProposalComment,
reportProposalComment,
},
)(ProposalComments);

View File

@ -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<any>) => {
// dispatch({
// type: types.POST_PROPOSAL_COMMENT,
// })
return async (dispatch: Dispatch<any>) => {
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,
});
}
};
}

View File

@ -172,6 +172,42 @@ function addPostedComment(state: ProposalState, payload: PostCommentPayload) {
};
}
function updateCommentInStore(
state: ProposalState,
commentId: Comment['id'],
update: Partial<Comment>,
) {
// 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,