From 744c90501ec283d3de65adfc9c80ee9b7dfd0f79 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 4 Oct 2018 15:02:59 -0400 Subject: [PATCH 1/4] Add comments to projects and replies to comments --- src/components/forms/inplace-input.scss | 4 +- src/redux/preview.js | 37 ++++ src/views/preview/comment/comment.jsx | 165 +++++++++------ src/views/preview/comment/comment.scss | 31 +++ src/views/preview/comment/compose-comment.jsx | 189 ++++++++++++++++-- .../preview/comment/top-level-comment.jsx | 14 +- src/views/preview/presentation.jsx | 15 ++ src/views/preview/preview.jsx | 9 + 8 files changed, 386 insertions(+), 78 deletions(-) diff --git a/src/components/forms/inplace-input.scss b/src/components/forms/inplace-input.scss index 33bc254b3..65b3094e1 100644 --- a/src/components/forms/inplace-input.scss +++ b/src/components/forms/inplace-input.scss @@ -36,7 +36,7 @@ } .inplace-textarea { - transition: all 1s ease; + transition: all .2s ease; border: 2px dashed $ui-blue-25percent; border-radius: 8px; background-color: $ui-light-gray; @@ -49,7 +49,7 @@ resize: none; &:focus { - transition: all 1s ease; + transition: all .2s ease; outline: none; border: 2px solid $ui-blue; box-shadow: 0 0 0 4px $ui-blue-25percent; diff --git a/src/redux/preview.js b/src/redux/preview.js index 2652f1379..e115cdd3c 100644 --- a/src/redux/preview.js +++ b/src/redux/preview.js @@ -95,6 +95,38 @@ module.exports.previewReducer = (state, action) => { return comment; }) }); + case 'ADD_NEW_COMMENT': + if (action.comment.parent_id) { + let topLevelParent = action.comment.parent_id; + + // If this is a nested reply, we need to look up which top level comment + // to put this new reply under. + if (!state.replies[topLevelParent]) { + Object.keys(state.replies).forEach(topLevelCommentId => { + state.replies[topLevelCommentId].forEach(reply => { + if (reply.id === action.comment.parent_id) { + topLevelParent = topLevelCommentId; + } + }); + }); + } + + if (state.replies[topLevelParent]) { + const replies = JSON.parse(JSON.stringify(state.replies)); + // Replies to comments go at the end of the thread + replies[topLevelParent] = replies[topLevelParent].concat(action.comment); + return Object.assign({}, state, {replies: replies}); + } + + log.error('Could not find top level parent to put reply in'); + return state; + } + + // Reply to the top level project, put the reply at the beginning + return Object.assign({}, state, { + comments: [action.comment, ...state.comments], + replies: Object.assign({}, state.replies, {[action.comment.id]: []}) + }); case 'SET_REPLIES': return Object.assign({}, state, { replies: merge({}, state.replies, action.replies) @@ -205,6 +237,11 @@ module.exports.setCommentDeleted = commentId => ({ commentId: commentId }); +module.exports.addNewComment = comment => ({ + type: 'ADD_NEW_COMMENT', + comment: comment +}); + module.exports.getProjectInfo = (id, token) => (dispatch => { const opts = { uri: `/projects/${id}` diff --git a/src/views/preview/comment/comment.jsx b/src/views/preview/comment/comment.jsx index 19ca6b131..25b0a9f94 100644 --- a/src/views/preview/comment/comment.jsx +++ b/src/views/preview/comment/comment.jsx @@ -1,76 +1,120 @@ const React = require('react'); const PropTypes = require('prop-types'); +const bindAll = require('lodash.bindall'); const classNames = require('classnames'); const FlexRow = require('../../../components/flex-row/flex-row.jsx'); const Avatar = require('../../../components/avatar/avatar.jsx'); const FormattedRelative = require('react-intl').FormattedRelative; +const ComposeComment = require('./compose-comment.jsx'); require('./comment.scss'); -const Comment = ({ - author, - deletable, - deleted, - content, - datetimeCreated, - onDelete, - id -}) => ( -
- - - - - - {author.username} -
- {deletable ? ( - - Delete {/* TODO internationalize */} - - ) : null} - - Report {/* TODO internationalize */} - -
-
+class Comment extends React.Component { + constructor (props) { + super(props); + + bindAll(this, [ + 'handlePostReply', + 'handleToggleReplying' + ]); + + this.state = { + replying: false + }; + } + + handlePostReply (comment) { + this.setState({replying: false}); + this.props.onAddComment(comment); + } + + handleToggleReplying () { + this.setState({replying: !this.state.replying}); + } + + render () { + const { + author, + deletable, + deleted, + canReply, + content, + datetimeCreated, + onDelete, + id, + projectId + } = this.props; + + return (
- {/* TODO: at the moment, comment content does not properly display - * emojis/easter eggs - * @user links in replies - * links to scratch.mit.edu pages - */} - {content} - - - - - + + + + + {author.username} +
+ {deletable ? ( + + Delete {/* TODO internationalize */} + + ) : null} + + Report {/* TODO internationalize */} + +
+
+
- reply - + {/* TODO: at the moment, comment content does not properly display + * emojis/easter eggs + * @user links in replies + * links to scratch.mit.edu pages + */} + {content} + + + + + {canReply ? ( + + reply + + ) : null} + +
+ {this.state.replying ? ( + + + + ) : null}
-
-
-); + ); + } +} Comment.propTypes = { author: PropTypes.shape({ @@ -78,12 +122,15 @@ Comment.propTypes = { image: PropTypes.string, username: PropTypes.string }), + canReply: PropTypes.bool, content: PropTypes.string, datetimeCreated: PropTypes.string, deletable: PropTypes.bool, deleted: PropTypes.bool, id: PropTypes.number, - onDelete: PropTypes.func + onAddComment: PropTypes.func, + onDelete: PropTypes.func, + projectId: PropTypes.number }; module.exports = Comment; diff --git a/src/views/preview/comment/comment.scss b/src/views/preview/comment/comment.scss index 6ebccbe75..17ae74717 100644 --- a/src/views/preview/comment/comment.scss +++ b/src/views/preview/comment/comment.scss @@ -2,6 +2,24 @@ .compose-comment { width: 100%; + margin-left: 0.5rem; + + .compose-error-row { + width: 100%; + justify-content: flex-start; + + .compose-error-tip { + width: 100%; + margin-bottom: 0.5rem; + border: 1px solid $active-gray; + border-radius: 5px; + background-color: $ui-orange; + padding: 0.25rem; + font-size: 0.85rem; + text-align: left; + color: $type-white; + } + } .textarea-row { width: 100%; @@ -161,6 +179,9 @@ .comment-reply { display: inline-flex; + cursor: pointer; + color: $ui-blue; + font-weight: bold; &:after { margin-left: .25rem; @@ -197,6 +218,16 @@ } } +.comments-root-reply { + margin-bottom: 1.5rem; +} + +.comment-reply-row { + width: 100%; + margin-top: 1.5rem; + margin-left: 0.5rem; +} + .expand-thread { margin-bottom: 24px; width: 100%; diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx index d14537965..243f5dc49 100644 --- a/src/views/preview/comment/compose-comment.jsx +++ b/src/views/preview/comment/compose-comment.jsx @@ -1,26 +1,185 @@ const React = require('react'); +const PropTypes = require('prop-types'); +const bindAll = require('lodash.bindall'); +const classNames = require('classnames'); +const keyMirror = require('keymirror'); const FlexRow = require('../../../components/flex-row/flex-row.jsx'); +const Avatar = require('../../../components/avatar/avatar.jsx'); const InplaceInput = require('../../../components/forms/inplace-input.jsx'); const Button = require('../../../components/forms/button.jsx'); +const connect = require('react-redux').connect; + +const api = require('../../../lib/api'); + require('./comment.scss'); const onUpdate = update => update; -const ComposeComment = () => ( - - - - - - 500 characters left - - -); +const MAX_COMMENT_LENGTH = 500; -module.exports = ComposeComment; +const ComposeStatus = keyMirror({ + EDITING: null, + SUBMITTING: null, + REJECTED: null +}); + +/* TODO translations */ +const CommentErrorMessages = { + isEmpty: "You can't post an empty comment", + isFlood: "Woah, seems like you're commenting really quickly. Please wait longer between posts.", + isBad: 'Hmm...the bad word detector thinks there is a problem with your comment. ' + + 'Please change it and remember to be respectful.', + serverError: 'Server error, please try again later', + + /* TODO others... */ + isSpam: '', + isMuted: '', + isUnconstructive: '', + isDisallowed: '', + isIPMuted: '', + isTooLong: '', + isNotPermitted: '' +}; + +class ComposeComment extends React.Component { + constructor (props) { + super(props); + + bindAll(this, [ + 'handlePost', + 'handleCancel', + 'handleInput' + ]); + + this.state = { + message: '', + status: ComposeStatus.EDITING, + error: null + }; + } + handleInput (event) { + this.setState({ + message: event.target.value, + status: ComposeStatus.EDITING, + error: null + }); + } + handlePost () { + this.setState({status: ComposeStatus.SUBMITTING}); + api({ + uri: `/proxy/comments/project/${this.props.projectId}`, + authentication: this.props.user.token, + withCredentials: true, + method: 'POST', + useCsrf: true, + json: { + content: this.state.message, + parent_id: this.props.parentId || '', + comentee_id: this.props.comenteeId || '' + } + }, (err, body, res) => { + if (err || res.statusCode !== 200) { + body = {rejected: 'serverError'}; + } + + if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) { + this.setState({status: ComposeStatus.REJECTED, error: body.rejected}); + return; + } + + // Clear the text field on successful submission + this.setState({status: ComposeStatus.EDITING, error: null, message: ''}); + + // Add the username, which isn't included right now from scratch-api + if (body.author) body.author.username = this.props.user.username; + + this.props.onAddComment(body); + }); + } + handleCancel () { + this.setState({message: '', error: null, status: ComposeStatus.EDITING}); + if (this.props.onCancel) this.props.onCancel(); + } + render () { + return ( +
+ + + + + {this.state.error ? ( + +
+ {CommentErrorMessages[this.state.error] || 'Unknown error'} +
+
+ ) : null} + = 0 ? 'compose-valid' : 'compose-invalid')} + handleUpdate={onUpdate} + name="compose-comment" + type="textarea" + value={this.state.message} + onInput={this.handleInput} + /> + + + + = 0 ? + 'compose-valid' : 'compose-invalid')} + > + {/* TODO internationalize */} + {MAX_COMMENT_LENGTH - this.state.message.length} characters left + + +
+
+ ); + } +} + +ComposeComment.propTypes = { + comenteeId: PropTypes.number, + onAddComment: PropTypes.func, + onCancel: PropTypes.func, + parentId: PropTypes.number, + projectId: PropTypes.number, + user: PropTypes.shape({ + id: PropTypes.number, + username: PropTypes.string, + token: PropTypes.string, + thumbnailUrl: PropTypes.string + }) +}; + +const mapStateToProps = state => ({ + user: state.session.session.user +}); + +const ConnectedComposeComment = connect( + mapStateToProps +)(ComposeComment); + +module.exports = ConnectedComposeComment; diff --git a/src/views/preview/comment/top-level-comment.jsx b/src/views/preview/comment/top-level-comment.jsx index f3eb129ad..77a2bed28 100644 --- a/src/views/preview/comment/top-level-comment.jsx +++ b/src/views/preview/comment/top-level-comment.jsx @@ -33,19 +33,24 @@ class TopLevelComment extends React.Component { render () { const { author, + canReply, content, datetimeCreated, deletable, deleted, id, - replies + replies, + projectId, + onAddComment } = this.props; return ( {replies.length > 0 && ( ))} @@ -87,11 +95,13 @@ TopLevelComment.propTypes = { image: PropTypes.string, username: PropTypes.string }), + canReply: PropTypes.bool, content: PropTypes.string, datetimeCreated: PropTypes.string, deletable: PropTypes.bool, deleted: PropTypes.bool, id: PropTypes.number, + onAddComment: PropTypes.func, onDelete: PropTypes.func, parentId: PropTypes.number, projectId: PropTypes.string, diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index ee3dbab5a..6824de137 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -21,6 +21,7 @@ const StudioList = require('./studio-list.jsx'); const Subactions = require('./subactions.jsx'); const InplaceInput = require('../../components/forms/inplace-input.jsx'); const TopLevelComment = require('./comment/top-level-comment.jsx'); +const ComposeComment = require('./comment/compose-comment.jsx'); const ExtensionChip = require('./extension-chip.jsx'); const projectShape = require('./projectshape.jsx').projectShape; @@ -64,6 +65,7 @@ const PreviewPresentation = ({ projectStudios, studios, userOwnsProject, + onAddComment, onDeleteComment, onFavoriteClicked, onLoadMore, @@ -317,10 +319,21 @@ const PreviewPresentation = ({

Comments

{/* TODO: Add toggle comments component and logic*/}
+ + + {isLoggedIn && + + } + + {comments.map(comment => ( ))} @@ -374,6 +388,7 @@ PreviewPresentation.propTypes = { isShared: PropTypes.bool, loveCount: PropTypes.number, loved: PropTypes.bool, + onAddComment: PropTypes.func, onAddToStudioClicked: PropTypes.func, onAddToStudioClosed: PropTypes.func, onDeleteComment: PropTypes.func, diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx index e7aed39e9..a4ea7d051 100644 --- a/src/views/preview/preview.jsx +++ b/src/views/preview/preview.jsx @@ -33,6 +33,7 @@ class Preview extends React.Component { super(props); bindAll(this, [ 'addEventListeners', + 'handleAddComment', 'handleDeleteComment', 'handleToggleStudio', 'handleFavoriteToggle', @@ -166,6 +167,9 @@ class Preview extends React.Component { }); }); } + handleAddComment (comment) { + this.props.handleAddComment(comment); + } handleDeleteComment (id) { this.props.handleDeleteComment(this.state.projectId, id, this.props.user.token); } @@ -341,6 +345,7 @@ class Preview extends React.Component { reportOpen={this.state.reportOpen} studios={this.props.studios} userOwnsProject={this.props.userOwnsProject} + onAddComment={this.handleAddComment} onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClosed={this.handleAddToStudioClose} onDeleteComment={this.handleDeleteComment} @@ -399,6 +404,7 @@ Preview.propTypes = { getProjectStudios: PropTypes.func.isRequired, getRemixes: PropTypes.func.isRequired, getTopLevelComments: PropTypes.func.isRequired, + handleAddComment: PropTypes.func, handleDeleteComment: PropTypes.func, handleLogIn: PropTypes.func, handleLogOut: PropTypes.func, @@ -526,6 +532,9 @@ const mapStateToProps = state => { }; const mapDispatchToProps = dispatch => ({ + handleAddComment: comment => { + dispatch(previewActions.addNewComment(comment)); + }, handleDeleteComment: (projectId, commentId, token) => { dispatch(previewActions.deleteComment(projectId, commentId, token)); }, From 24fe4fef65d87700aeca111847dbecbd387372cb Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 5 Oct 2018 13:08:51 -0400 Subject: [PATCH 2/4] Use topLevelCommentId to simplify adding and deleting comments This fixes the nested comment deletion problem: https://github.com/LLK/scratch-www/issues/2151 --- src/redux/preview.js | 54 +++++++++---------- src/views/preview/comment/comment.jsx | 8 ++- .../preview/comment/top-level-comment.jsx | 26 +++++---- src/views/preview/preview.jsx | 16 +++--- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/redux/preview.js b/src/redux/preview.js index e115cdd3c..a151fe41c 100644 --- a/src/redux/preview.js +++ b/src/redux/preview.js @@ -87,6 +87,19 @@ module.exports.previewReducer = (state, action) => { comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this? }); case 'SET_COMMENT_DELETED': + if (action.topLevelCommentId) { + return Object.assign({}, state, { + replies: Object.assign({}, state.replies, { + [action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => { + if (comment.id === action.commentId) { + return Object.assign({}, comment, {deleted: true}); + } + return comment; + }) + }) + }); + } + return Object.assign({}, state, { comments: state.comments.map(comment => { if (comment.id === action.commentId) { @@ -96,30 +109,13 @@ module.exports.previewReducer = (state, action) => { }) }); case 'ADD_NEW_COMMENT': - if (action.comment.parent_id) { - let topLevelParent = action.comment.parent_id; - - // If this is a nested reply, we need to look up which top level comment - // to put this new reply under. - if (!state.replies[topLevelParent]) { - Object.keys(state.replies).forEach(topLevelCommentId => { - state.replies[topLevelCommentId].forEach(reply => { - if (reply.id === action.comment.parent_id) { - topLevelParent = topLevelCommentId; - } - }); - }); - } - - if (state.replies[topLevelParent]) { - const replies = JSON.parse(JSON.stringify(state.replies)); - // Replies to comments go at the end of the thread - replies[topLevelParent] = replies[topLevelParent].concat(action.comment); - return Object.assign({}, state, {replies: replies}); - } - - log.error('Could not find top level parent to put reply in'); - return state; + if (action.topLevelCommentId) { + return Object.assign({}, state, { + replies: Object.assign({}, state.replies, { + // Replies to comments go at the end of the thread + [action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment) + }) + }); } // Reply to the top level project, put the reply at the beginning @@ -232,14 +228,16 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({ status: status }); -module.exports.setCommentDeleted = commentId => ({ +module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({ type: 'SET_COMMENT_DELETED', - commentId: commentId + commentId: commentId, + topLevelCommentId: topLevelCommentId }); -module.exports.addNewComment = comment => ({ +module.exports.addNewComment = (comment, topLevelCommentId) => ({ type: 'ADD_NEW_COMMENT', - comment: comment + comment: comment, + topLevelCommentId: topLevelCommentId }); module.exports.getProjectInfo = (id, token) => (dispatch => { diff --git a/src/views/preview/comment/comment.jsx b/src/views/preview/comment/comment.jsx index 25b0a9f94..378a76b24 100644 --- a/src/views/preview/comment/comment.jsx +++ b/src/views/preview/comment/comment.jsx @@ -15,6 +15,7 @@ class Comment extends React.Component { super(props); bindAll(this, [ + 'handleDelete', 'handlePostReply', 'handleToggleReplying' ]); @@ -33,6 +34,10 @@ class Comment extends React.Component { this.setState({replying: !this.state.replying}); } + handleDelete () { + this.props.onDelete(this.props.id); + } + render () { const { author, @@ -41,7 +46,6 @@ class Comment extends React.Component { canReply, content, datetimeCreated, - onDelete, id, projectId } = this.props; @@ -64,7 +68,7 @@ class Comment extends React.Component { {deletable ? ( Delete {/* TODO internationalize */} diff --git a/src/views/preview/comment/top-level-comment.jsx b/src/views/preview/comment/top-level-comment.jsx index 77a2bed28..1f0899970 100644 --- a/src/views/preview/comment/top-level-comment.jsx +++ b/src/views/preview/comment/top-level-comment.jsx @@ -13,7 +13,8 @@ class TopLevelComment extends React.Component { super(props); bindAll(this, [ 'handleExpandThread', - 'handleDelete' + 'handleAddComment', + 'handleDeleteComment' ]); this.state = { expanded: false @@ -26,8 +27,14 @@ class TopLevelComment extends React.Component { }); } - handleDelete () { - this.props.onDelete(this.props.id); + handleDeleteReply (commentId) { + // Only apply topLevelCommentId for deleting replies + // The top level comment itself just gets passed onDelete directly + this.props.onDelete(commentId, this.props.id); + } + + handleAddComment (comment) { + this.props.onAddComment(comment, this.props.id); } render () { @@ -39,18 +46,17 @@ class TopLevelComment extends React.Component { deletable, deleted, id, + onDelete, replies, - projectId, - onAddComment + projectId } = this.props; return ( {replies.length > 0 && ))} diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx index a4ea7d051..76ed08b89 100644 --- a/src/views/preview/preview.jsx +++ b/src/views/preview/preview.jsx @@ -167,11 +167,11 @@ class Preview extends React.Component { }); }); } - handleAddComment (comment) { - this.props.handleAddComment(comment); + handleAddComment (comment, topLevelCommentId) { + this.props.handleAddComment(comment, topLevelCommentId); } - handleDeleteComment (id) { - this.props.handleDeleteComment(this.state.projectId, id, this.props.user.token); + handleDeleteComment (id, topLevelCommentId) { + this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); } handleReportClick () { this.setState({reportOpen: true}); @@ -532,11 +532,11 @@ const mapStateToProps = state => { }; const mapDispatchToProps = dispatch => ({ - handleAddComment: comment => { - dispatch(previewActions.addNewComment(comment)); + handleAddComment: (comment, topLevelCommentId) => { + dispatch(previewActions.addNewComment(comment, topLevelCommentId)); }, - handleDeleteComment: (projectId, commentId, token) => { - dispatch(previewActions.deleteComment(projectId, commentId, token)); + handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => { + dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token)); }, handleOpenRegistration: event => { event.preventDefault(); From 9b122470f592f4add7ddf49cfec71ebb5d6b80b0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 5 Oct 2018 13:13:40 -0400 Subject: [PATCH 3/4] Fix formatting from review comments --- src/views/preview/comment/comment.jsx | 2 -- src/views/preview/comment/comment.scss | 14 ++++++------ src/views/preview/comment/compose-comment.jsx | 22 ++++++++++++++----- .../preview/comment/top-level-comment.jsx | 2 +- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/views/preview/comment/comment.jsx b/src/views/preview/comment/comment.jsx index 378a76b24..d5e4bede0 100644 --- a/src/views/preview/comment/comment.jsx +++ b/src/views/preview/comment/comment.jsx @@ -13,13 +13,11 @@ require('./comment.scss'); class Comment extends React.Component { constructor (props) { super(props); - bindAll(this, [ 'handleDelete', 'handlePostReply', 'handleToggleReplying' ]); - this.state = { replying: false }; diff --git a/src/views/preview/comment/comment.scss b/src/views/preview/comment/comment.scss index 17ae74717..48d741a48 100644 --- a/src/views/preview/comment/comment.scss +++ b/src/views/preview/comment/comment.scss @@ -1,23 +1,23 @@ @import "../../../colors"; .compose-comment { + margin-left: .5rem; width: 100%; - margin-left: 0.5rem; .compose-error-row { width: 100%; justify-content: flex-start; .compose-error-tip { - width: 100%; - margin-bottom: 0.5rem; + margin-bottom: .5rem; border: 1px solid $active-gray; border-radius: 5px; background-color: $ui-orange; - padding: 0.25rem; - font-size: 0.85rem; + padding: .25rem; + width: 100%; text-align: left; color: $type-white; + font-size: .85rem; } } @@ -223,9 +223,9 @@ } .comment-reply-row { - width: 100%; margin-top: 1.5rem; - margin-left: 0.5rem; + margin-left: .5rem; + width: 100%; } .expand-thread { diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx index 243f5dc49..2c0535bad 100644 --- a/src/views/preview/comment/compose-comment.jsx +++ b/src/views/preview/comment/compose-comment.jsx @@ -46,13 +46,11 @@ const CommentErrorMessages = { class ComposeComment extends React.Component { constructor (props) { super(props); - bindAll(this, [ 'handlePost', 'handleCancel', 'handleInput' ]); - this.state = { message: '', status: ComposeStatus.EDITING, @@ -85,12 +83,20 @@ class ComposeComment extends React.Component { } if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) { - this.setState({status: ComposeStatus.REJECTED, error: body.rejected}); + // Note: does not reset the message state + this.setState({ + status: ComposeStatus.REJECTED, + error: body.rejected + }); return; } - // Clear the text field on successful submission - this.setState({status: ComposeStatus.EDITING, error: null, message: ''}); + // Clear the text field and reset status on successful submission + this.setState({ + message: '', + status: ComposeStatus.EDITING, + error: null + }); // Add the username, which isn't included right now from scratch-api if (body.author) body.author.username = this.props.user.username; @@ -99,7 +105,11 @@ class ComposeComment extends React.Component { }); } handleCancel () { - this.setState({message: '', error: null, status: ComposeStatus.EDITING}); + this.setState({ + message: '', + status: ComposeStatus.EDITING, + error: null + }); if (this.props.onCancel) this.props.onCancel(); } render () { diff --git a/src/views/preview/comment/top-level-comment.jsx b/src/views/preview/comment/top-level-comment.jsx index 1f0899970..6cc67da91 100644 --- a/src/views/preview/comment/top-level-comment.jsx +++ b/src/views/preview/comment/top-level-comment.jsx @@ -14,7 +14,7 @@ class TopLevelComment extends React.Component { bindAll(this, [ 'handleExpandThread', 'handleAddComment', - 'handleDeleteComment' + 'handleDeleteReply' ]); this.state = { expanded: false From 58ab51a6297d2c8ae22d35fd805475a6bf8565d3 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Sun, 7 Oct 2018 11:01:20 -0400 Subject: [PATCH 4/4] Add messages for other rejected comment reasons --- src/views/preview/comment/compose-comment.jsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx index 2c0535bad..0db753c58 100644 --- a/src/views/preview/comment/compose-comment.jsx +++ b/src/views/preview/comment/compose-comment.jsx @@ -31,16 +31,21 @@ const CommentErrorMessages = { isFlood: "Woah, seems like you're commenting really quickly. Please wait longer between posts.", isBad: 'Hmm...the bad word detector thinks there is a problem with your comment. ' + 'Please change it and remember to be respectful.', - serverError: 'Server error, please try again later', - - /* TODO others... */ - isSpam: '', - isMuted: '', - isUnconstructive: '', - isDisallowed: '', - isIPMuted: '', - isTooLong: '', - isNotPermitted: '' + hasChatSite: 'Uh oh! This comment contains a link to a website with unmoderated chat.' + + 'For safety reasons, please do not link to these sites!', + isSpam: "Hmm, seems like you've posted the same comment a bunch of times. Please don't spam.", + isMuted: "Hmm, the filterbot is pretty sure your recent comments weren't ok for Scratch, " + + 'so your account has been muted for the rest of the day. :/', + isUnconstructive: 'Hmm, the filterbot thinks your comment may be mean or disrespectful. ' + + 'Remember, most projects on Scratch are made by people who are just learning how to program.', + isDisallowed: 'Hmm, it looks like comments have been turned off for this page. :/', + // TODO implement the special modal for ip mute bans that includes links to appeals + // this is just a stub of the actual message + isIPMuted: 'Sorry, the Scratch Team had to prevent your network from sharing comments or ' + + 'projects because it was used to break our community guidelines too many times.' + + 'You can still share comments and projects from another network.', + isTooLong: "That's too long! Please find a way to shorten your text.", + error: 'Oops! Something went wrong' }; class ComposeComment extends React.Component { @@ -79,14 +84,17 @@ class ComposeComment extends React.Component { } }, (err, body, res) => { if (err || res.statusCode !== 200) { - body = {rejected: 'serverError'}; + body = {rejected: 'error'}; } if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) { // Note: does not reset the message state this.setState({ status: ComposeStatus.REJECTED, - error: body.rejected + // If there is a special error message for the rejected reason, + // use it. Otherwise, use the generic 'error' ("Oops!...") + error: CommentErrorMessages[body.rejected] ? + body.rejected : 'error' }); return; }