Merge pull request #2164 from paulkaplan/comment-report-delete

Comment report action and modal flow for deleting
This commit is contained in:
Paul Kaplan 2018-10-11 11:08:07 -04:00 committed by GitHub
commit 1f8342b987
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 385 additions and 21 deletions

View file

@ -0,0 +1,81 @@
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const Button = require('../../forms/button.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const DeleteModal = ({
intl,
onDelete,
onReport,
onRequestClose,
...modalProps
}) => (
<Modal
useStandardSizes
className="mod-report"
contentLabel={intl.formatMessage({id: 'comments.deleteModal.title'})}
onRequestClose={onRequestClose}
{...modalProps}
>
<div>
<div className="report-modal-header">
<div className="report-content-label">
<FormattedMessage id="comments.deleteModal.title" />
</div>
</div>
<div className="report-modal-content">
<div>
<div className="instructions">
<FormattedMessage id="comments.deleteModal.body" />
</div>
</div>
</div>
<FlexRow className="action-buttons">
<div className="action-buttons-overflow-fix">
<Button
className="action-button submit-button"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
<Button
className="action-button submit-button"
type="button"
onClick={onReport}
>
<FormattedMessage id="comments.report" />
</Button>
<Button
className="action-button submit-button"
type="button"
onClick={onDelete}
>
<FormattedMessage id="comments.delete" />
</Button>
</div>
</FlexRow>
</div>
</Modal>
);
DeleteModal.propTypes = {
intl: intlShape,
onDelete: PropTypes.func,
onReport: PropTypes.func,
onRequestClose: PropTypes.func
};
module.exports = injectIntl(DeleteModal);

View file

@ -0,0 +1,44 @@
@import "../../../colors";
@import "../../../frameless";
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
.mod-report * {
box-sizing: border-box;
}
.mod-report {
margin: 100px auto;
outline: none;
padding: 0;
width: 36.25rem; /* 580px; */
user-select: none;
}
.report-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
background-color: $ui-coral;
padding-top: .75rem;
width: 100%;
height: 3rem;
box-sizing: border-box;
}
.report-content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.report-modal-content {
margin: 1rem auto;
width: 80%;
font-size: .875rem;
.instructions {
line-height: 1.5rem;
}
}

View file

@ -0,0 +1,84 @@
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const Button = require('../../forms/button.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const ReportModal = ({
intl,
isConfirmed,
onReport,
onRequestClose,
...modalProps
}) => (
<Modal
useStandardSizes
className="mod-report"
contentLabel={intl.formatMessage({id: 'comments.reportModal.title'})}
onRequestClose={onRequestClose}
{...modalProps}
>
<div>
<div className="report-modal-header">
<div className="report-content-label">
<FormattedMessage id="comments.reportModal.title" />
</div>
</div>
<div className="report-modal-content">
<div>
<div className="instructions">
{isConfirmed ? (
<FormattedMessage id="comments.reportModal.reported" />
) : (
<FormattedMessage id="comments.reportModal.prompt" />
)}
</div>
</div>
</div>
<FlexRow className="action-buttons">
<div className="action-buttons-overflow-fix">
<Button
className="action-button submit-button"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
{isConfirmed ? null : (
<Button
className="action-button submit-button"
type="button"
onClick={onReport}
>
<div className="action-button-text">
<FormattedMessage id="comments.report" />
</div>
</Button>
)}
</div>
</FlexRow>
</div>
</Modal>
);
ReportModal.propTypes = {
intl: intlShape,
isConfirmed: PropTypes.bool,
isOwnSpace: PropTypes.bool,
onReport: PropTypes.func,
onRequestClose: PropTypes.func,
type: PropTypes.string
};
module.exports = injectIntl(ReportModal);

View file

@ -202,5 +202,14 @@
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.", "report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
"report.send": "Send", "report.send": "Send",
"report.sending": "Sending...", "report.sending": "Sending...",
"report.textMissing": "Please tell us why you are reporting this project" "report.textMissing": "Please tell us why you are reporting this project",
"comments.report": "Report",
"comments.delete": "Delete",
"comments.reportModal.title": "Report Comment",
"comments.reportModal.reported": "The comment has been reported, and the Scratch Team has been notified.",
"comments.reportModal.prompt": "Are you sure you want to report this comment?",
"comments.deleteModal.title": "Delete Comment",
"comments.deleteModal.body": "Delete this comment? If the comment is mean or disrespectful, please click Report instead to let the Scratch Team know about it.",
"comments.reply": "reply"
} }

View file

@ -86,13 +86,13 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, { return Object.assign({}, state, {
comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this? comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this?
}); });
case 'SET_COMMENT_DELETED': case 'UPDATE_COMMENT':
if (action.topLevelCommentId) { if (action.topLevelCommentId) {
return Object.assign({}, state, { return Object.assign({}, state, {
replies: Object.assign({}, state.replies, { replies: Object.assign({}, state.replies, {
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => { [action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => {
if (comment.id === action.commentId) { if (comment.id === action.commentId) {
return Object.assign({}, comment, {deleted: true}); return Object.assign({}, comment, action.comment);
} }
return comment; return comment;
}) })
@ -103,7 +103,7 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, { return Object.assign({}, state, {
comments: state.comments.map(comment => { comments: state.comments.map(comment => {
if (comment.id === action.commentId) { if (comment.id === action.commentId) {
return Object.assign({}, comment, {deleted: true}); return Object.assign({}, comment, action.comment);
} }
return comment; return comment;
}) })
@ -229,9 +229,21 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
}); });
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({ module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
type: 'SET_COMMENT_DELETED', type: 'UPDATE_COMMENT',
commentId: commentId, commentId: commentId,
topLevelCommentId: topLevelCommentId topLevelCommentId: topLevelCommentId,
comment: {
deleted: true
}
});
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
reported: true
}
}); });
module.exports.addNewComment = (comment, topLevelCommentId) => ({ module.exports.addNewComment = (comment, topLevelCommentId) => ({
@ -611,7 +623,7 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
}); });
}); });
module.exports.deleteComment = (projectId, commentId, token) => (dispatch => { module.exports.deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */ /* TODO fetching/fetched/error states updates for comment deleting */
api({ api({
uri: `/proxy/comments/project/${projectId}`, uri: `/proxy/comments/project/${projectId}`,
@ -627,7 +639,24 @@ module.exports.deleteComment = (projectId, commentId, token) => (dispatch => {
log.error(err || res.body); log.error(err || res.body);
return; return;
} }
dispatch(module.exports.setCommentDeleted(commentId)); dispatch(module.exports.setCommentDeleted(commentId, topLevelCommentId));
});
});
module.exports.reportComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/project/${projectId}/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(module.exports.setCommentReported(commentId, topLevelCommentId));
}); });
}); });

View file

@ -6,7 +6,10 @@ const classNames = require('classnames');
const FlexRow = require('../../../components/flex-row/flex-row.jsx'); const FlexRow = require('../../../components/flex-row/flex-row.jsx');
const Avatar = require('../../../components/avatar/avatar.jsx'); const Avatar = require('../../../components/avatar/avatar.jsx');
const FormattedRelative = require('react-intl').FormattedRelative; const FormattedRelative = require('react-intl').FormattedRelative;
const FormattedMessage = require('react-intl').FormattedMessage;
const ComposeComment = require('./compose-comment.jsx'); const ComposeComment = require('./compose-comment.jsx');
const DeleteCommentModal = require('../../../components/modal/comments/delete-comment.jsx');
const ReportCommentModal = require('../../../components/modal/comments/report-comment.jsx');
require('./comment.scss'); require('./comment.scss');
@ -15,10 +18,18 @@ class Comment extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleDelete', 'handleDelete',
'handleCancelDelete',
'handleConfirmDelete',
'handleReport',
'handleConfirmReport',
'handleCancelReport',
'handlePostReply', 'handlePostReply',
'handleToggleReplying' 'handleToggleReplying'
]); ]);
this.state = { this.state = {
deleting: false,
reporting: false,
reportConfirmed: false,
replying: false replying: false
}; };
} }
@ -33,9 +44,39 @@ class Comment extends React.Component {
} }
handleDelete () { handleDelete () {
this.setState({deleting: true});
}
handleConfirmDelete () {
this.setState({deleting: false});
this.props.onDelete(this.props.id); this.props.onDelete(this.props.id);
} }
handleCancelDelete () {
this.setState({deleting: false});
}
handleReport () {
this.setState({reporting: true});
}
handleConfirmReport () {
this.setState({
reporting: false,
reportConfirmed: true,
deleting: false // To close delete modal if reported from delete modal
});
this.props.onReport(this.props.id);
}
handleCancelReport () {
this.setState({
reporting: false,
reportConfirmed: false
});
}
render () { render () {
const { const {
author, author,
@ -45,7 +86,8 @@ class Comment extends React.Component {
content, content,
datetimeCreated, datetimeCreated,
id, id,
projectId projectId,
reported
} = this.props; } = this.props;
return ( return (
@ -68,18 +110,22 @@ class Comment extends React.Component {
className="comment-delete" className="comment-delete"
onClick={this.handleDelete} onClick={this.handleDelete}
> >
Delete {/* TODO internationalize */} <FormattedMessage id="comments.delete" />
</span> </span>
) : null} ) : null}
<span className="comment-report"> <span
Report {/* TODO internationalize */} className="comment-report"
onClick={this.handleReport}
>
<FormattedMessage id="comments.report" />
</span> </span>
</div> </div>
</FlexRow> </FlexRow>
<div <div
className={classNames({ className={classNames({
'comment-bubble': true, 'comment-bubble': true,
'comment-bubble-deleted': deleted 'comment-bubble-deleted': deleted,
'comment-bubble-reported': reported
})} })}
> >
{/* TODO: at the moment, comment content does not properly display {/* TODO: at the moment, comment content does not properly display
@ -97,11 +143,12 @@ class Comment extends React.Component {
className="comment-reply" className="comment-reply"
onClick={this.handleToggleReplying} onClick={this.handleToggleReplying}
> >
reply <FormattedMessage id="comments.reply" />
</span> </span>
) : null} ) : null}
</FlexRow> </FlexRow>
</div> </div>
{this.state.replying ? ( {this.state.replying ? (
<FlexRow className="comment-reply-row"> <FlexRow className="comment-reply-row">
<ComposeComment <ComposeComment
@ -113,6 +160,24 @@ class Comment extends React.Component {
</FlexRow> </FlexRow>
) : null} ) : null}
</FlexRow> </FlexRow>
{this.state.deleting ? (
<DeleteCommentModal
isOpen
key="delete-comment-modal"
onDelete={this.handleConfirmDelete}
onReport={this.handleConfirmReport}
onRequestClose={this.handleCancelDelete}
/>
) : null}
{(this.state.reporting || this.state.reportConfirmed) ? (
<ReportCommentModal
isOpen
isConfirmed={this.state.reportConfirmed}
key="report-comment-modal"
onReport={this.handleConfirmReport}
onRequestClose={this.handleCancelReport}
/>
) : null}
</div> </div>
); );
} }
@ -132,7 +197,9 @@ Comment.propTypes = {
id: PropTypes.number, id: PropTypes.number,
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
projectId: PropTypes.number onReport: PropTypes.func,
projectId: PropTypes.string,
reported: PropTypes.bool
}; };
module.exports = Comment; module.exports = Comment;

View file

@ -151,8 +151,8 @@
} }
&.comment-bubble-deleted { &.comment-bubble-deleted {
$deleted-outline: #ff6680; $deleted-outline: $active-gray;
$deleted-background: rgb(236, 206, 223); $deleted-background: rgb(215, 222, 234);
border-color: $deleted-outline; border-color: $deleted-outline;
background-color: $deleted-background; background-color: $deleted-background;
@ -162,6 +162,19 @@
background: $deleted-background; background: $deleted-background;
} }
} }
&.comment-bubble-reported {
$reported-outline: #ff6680;
$reported-background: rgb(236, 206, 223);
border-color: $reported-outline;
background-color: $reported-background;
&:before {
border-color: $reported-outline transparent $reported-outline $reported-outline;
background: $reported-background;
}
}
} }
.comment-content { .comment-content {

View file

@ -183,7 +183,7 @@ ComposeComment.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.number, projectId: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
username: PropTypes.string, username: PropTypes.string,

View file

@ -14,7 +14,8 @@ class TopLevelComment extends React.Component {
bindAll(this, [ bindAll(this, [
'handleExpandThread', 'handleExpandThread',
'handleAddComment', 'handleAddComment',
'handleDeleteReply' 'handleDeleteReply',
'handleReportReply'
]); ]);
this.state = { this.state = {
expanded: false expanded: false
@ -33,6 +34,12 @@ class TopLevelComment extends React.Component {
this.props.onDelete(commentId, this.props.id); this.props.onDelete(commentId, this.props.id);
} }
handleReportReply (commentId) {
// Only apply topLevelCommentId for reporting replies
// The top level comment itself just gets passed onReport directly
this.props.onReport(commentId, this.props.id);
}
handleAddComment (comment) { handleAddComment (comment) {
this.props.onAddComment(comment, this.props.id); this.props.onAddComment(comment, this.props.id);
} }
@ -47,7 +54,9 @@ class TopLevelComment extends React.Component {
deleted, deleted,
id, id,
onDelete, onDelete,
onReport,
replies, replies,
reported,
projectId projectId
} = this.props; } = this.props;
@ -56,7 +65,18 @@ class TopLevelComment extends React.Component {
<Comment <Comment
projectId={projectId} projectId={projectId}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
{...{author, content, datetimeCreated, deletable, deleted, canReply, id, onDelete}} {...{
author,
content,
datetimeCreated,
deletable,
deleted,
canReply,
id,
onDelete,
onReport,
reported
}}
/> />
{replies.length > 0 && {replies.length > 0 &&
<FlexRow <FlexRow
@ -78,8 +98,10 @@ class TopLevelComment extends React.Component {
id={reply.id} id={reply.id}
key={reply.id} key={reply.id}
projectId={projectId} projectId={projectId}
reported={reply.reported}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
onDelete={this.handleDeleteReply} onDelete={this.handleDeleteReply}
onReport={this.handleReportReply}
/> />
))} ))}
</FlexRow> </FlexRow>
@ -109,9 +131,11 @@ TopLevelComment.propTypes = {
id: PropTypes.number, id: PropTypes.number,
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onReport: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, projectId: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object) replies: PropTypes.arrayOf(PropTypes.object),
reported: PropTypes.bool
}; };
module.exports = TopLevelComment; module.exports = TopLevelComment;

View file

@ -74,6 +74,7 @@ const PreviewPresentation = ({
onLoveClicked, onLoveClicked,
onReportClicked, onReportClicked,
onReportClose, onReportClose,
onReportComment,
onReportSubmit, onReportSubmit,
onAddToStudioClicked, onAddToStudioClicked,
onAddToStudioClosed, onAddToStudioClosed,
@ -367,8 +368,10 @@ const PreviewPresentation = ({
parentId={comment.parent_id} parentId={comment.parent_id}
projectId={projectId} projectId={projectId}
replies={replies && replies[comment.id] ? replies[comment.id] : []} replies={replies && replies[comment.id] ? replies[comment.id] : []}
reported={comment.reported}
onAddComment={onAddComment} onAddComment={onAddComment}
onDelete={onDeleteComment} onDelete={onDeleteComment}
onReport={onReportComment}
/> />
))} ))}
{comments.length < projectInfo.stats.comments && {comments.length < projectInfo.stats.comments &&
@ -421,6 +424,7 @@ PreviewPresentation.propTypes = {
onLoveClicked: PropTypes.func, onLoveClicked: PropTypes.func,
onReportClicked: PropTypes.func.isRequired, onReportClicked: PropTypes.func.isRequired,
onReportClose: PropTypes.func.isRequired, onReportClose: PropTypes.func.isRequired,
onReportComment: PropTypes.func.isRequired,
onReportSubmit: PropTypes.func.isRequired, onReportSubmit: PropTypes.func.isRequired,
onSeeInside: PropTypes.func, onSeeInside: PropTypes.func,
onToggleComments: PropTypes.func, onToggleComments: PropTypes.func,

View file

@ -42,6 +42,7 @@ class Preview extends React.Component {
'handlePopState', 'handlePopState',
'handleReportClick', 'handleReportClick',
'handleReportClose', 'handleReportClose',
'handleReportComment',
'handleReportSubmit', 'handleReportSubmit',
'handleAddToStudioClick', 'handleAddToStudioClick',
'handleAddToStudioClose', 'handleAddToStudioClose',
@ -182,6 +183,9 @@ class Preview extends React.Component {
handleDeleteComment (id, topLevelCommentId) { handleDeleteComment (id, topLevelCommentId) {
this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
} }
handleReportComment (id, topLevelCommentId) {
this.props.handleReportComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
}
handleReportClick () { handleReportClick () {
this.setState({reportOpen: true}); this.setState({reportOpen: true});
} }
@ -363,6 +367,7 @@ class Preview extends React.Component {
onLoveClicked={this.handleLoveToggle} onLoveClicked={this.handleLoveToggle}
onReportClicked={this.handleReportClick} onReportClicked={this.handleReportClick}
onReportClose={this.handleReportClose} onReportClose={this.handleReportClose}
onReportComment={this.handleReportComment}
onReportSubmit={this.handleReportSubmit} onReportSubmit={this.handleReportSubmit}
onSeeInside={this.handleSeeInside} onSeeInside={this.handleSeeInside}
onToggleComments={this.handleToggleComments} onToggleComments={this.handleToggleComments}
@ -419,6 +424,7 @@ Preview.propTypes = {
handleLogIn: PropTypes.func, handleLogIn: PropTypes.func,
handleLogOut: PropTypes.func, handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func, handleOpenRegistration: PropTypes.func,
handleReportComment: PropTypes.func,
handleToggleLoginOpen: PropTypes.func, handleToggleLoginOpen: PropTypes.func,
isEditable: PropTypes.bool, isEditable: PropTypes.bool,
isLoggedIn: PropTypes.bool, isLoggedIn: PropTypes.bool,
@ -548,6 +554,9 @@ const mapDispatchToProps = dispatch => ({
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => { handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token)); dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
}, },
handleReportComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token));
},
handleOpenRegistration: event => { handleOpenRegistration: event => {
event.preventDefault(); event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true)); dispatch(navigationActions.setRegistrationOpen(true));