mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 23:57:55 -05:00
Merge pull request #2150 from paulkaplan/new-comment
Add comments to projects and replies to comments
This commit is contained in:
commit
e29caceb6b
8 changed files with 421 additions and 89 deletions
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.inplace-textarea {
|
.inplace-textarea {
|
||||||
transition: all 1s ease;
|
transition: all .2s ease;
|
||||||
border: 2px dashed $ui-blue-25percent;
|
border: 2px dashed $ui-blue-25percent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: $ui-light-gray;
|
background-color: $ui-light-gray;
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
transition: all 1s ease;
|
transition: all .2s ease;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid $ui-blue;
|
border: 2px solid $ui-blue;
|
||||||
box-shadow: 0 0 0 4px $ui-blue-25percent;
|
box-shadow: 0 0 0 4px $ui-blue-25percent;
|
||||||
|
|
|
@ -87,6 +87,19 @@ module.exports.previewReducer = (state, action) => {
|
||||||
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 '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, {
|
return Object.assign({}, state, {
|
||||||
comments: state.comments.map(comment => {
|
comments: state.comments.map(comment => {
|
||||||
if (comment.id === action.commentId) {
|
if (comment.id === action.commentId) {
|
||||||
|
@ -95,6 +108,21 @@ module.exports.previewReducer = (state, action) => {
|
||||||
return comment;
|
return comment;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
case 'ADD_NEW_COMMENT':
|
||||||
|
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
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
comments: [action.comment, ...state.comments],
|
||||||
|
replies: Object.assign({}, state.replies, {[action.comment.id]: []})
|
||||||
|
});
|
||||||
case 'SET_REPLIES':
|
case 'SET_REPLIES':
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
replies: merge({}, state.replies, action.replies)
|
replies: merge({}, state.replies, action.replies)
|
||||||
|
@ -200,9 +228,16 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
|
||||||
status: status
|
status: status
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.setCommentDeleted = commentId => ({
|
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
|
||||||
type: 'SET_COMMENT_DELETED',
|
type: 'SET_COMMENT_DELETED',
|
||||||
commentId: commentId
|
commentId: commentId,
|
||||||
|
topLevelCommentId: topLevelCommentId
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.addNewComment = (comment, topLevelCommentId) => ({
|
||||||
|
type: 'ADD_NEW_COMMENT',
|
||||||
|
comment: comment,
|
||||||
|
topLevelCommentId: topLevelCommentId
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
||||||
|
|
|
@ -1,22 +1,54 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
const classNames = require('classnames');
|
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 ComposeComment = require('./compose-comment.jsx');
|
||||||
|
|
||||||
require('./comment.scss');
|
require('./comment.scss');
|
||||||
|
|
||||||
const Comment = ({
|
class Comment extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleDelete',
|
||||||
|
'handlePostReply',
|
||||||
|
'handleToggleReplying'
|
||||||
|
]);
|
||||||
|
this.state = {
|
||||||
|
replying: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePostReply (comment) {
|
||||||
|
this.setState({replying: false});
|
||||||
|
this.props.onAddComment(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleReplying () {
|
||||||
|
this.setState({replying: !this.state.replying});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete () {
|
||||||
|
this.props.onDelete(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
author,
|
author,
|
||||||
deletable,
|
deletable,
|
||||||
deleted,
|
deleted,
|
||||||
|
canReply,
|
||||||
content,
|
content,
|
||||||
datetimeCreated,
|
datetimeCreated,
|
||||||
onDelete,
|
id,
|
||||||
id
|
projectId
|
||||||
}) => (
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-row comment"
|
className="flex-row comment"
|
||||||
id={`comments-${id}`}
|
id={`comments-${id}`}
|
||||||
|
@ -34,7 +66,7 @@ const Comment = ({
|
||||||
{deletable ? (
|
{deletable ? (
|
||||||
<span
|
<span
|
||||||
className="comment-delete"
|
className="comment-delete"
|
||||||
onClick={onDelete}
|
onClick={this.handleDelete}
|
||||||
>
|
>
|
||||||
Delete {/* TODO internationalize */}
|
Delete {/* TODO internationalize */}
|
||||||
</span>
|
</span>
|
||||||
|
@ -60,17 +92,31 @@ const Comment = ({
|
||||||
<span className="comment-time">
|
<span className="comment-time">
|
||||||
<FormattedRelative value={new Date(datetimeCreated)} />
|
<FormattedRelative value={new Date(datetimeCreated)} />
|
||||||
</span>
|
</span>
|
||||||
<a
|
{canReply ? (
|
||||||
|
<span
|
||||||
className="comment-reply"
|
className="comment-reply"
|
||||||
href={`#comments-${id}`}
|
onClick={this.handleToggleReplying}
|
||||||
>
|
>
|
||||||
reply
|
reply
|
||||||
</a>
|
</span>
|
||||||
|
) : null}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.replying ? (
|
||||||
|
<FlexRow className="comment-reply-row">
|
||||||
|
<ComposeComment
|
||||||
|
parentId={id}
|
||||||
|
projectId={projectId}
|
||||||
|
onAddComment={this.handlePostReply}
|
||||||
|
onCancel={this.handleToggleReplying}
|
||||||
|
/>
|
||||||
|
</FlexRow>
|
||||||
|
) : null}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Comment.propTypes = {
|
Comment.propTypes = {
|
||||||
author: PropTypes.shape({
|
author: PropTypes.shape({
|
||||||
|
@ -78,12 +124,15 @@ Comment.propTypes = {
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
canReply: PropTypes.bool,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
datetimeCreated: PropTypes.string,
|
datetimeCreated: PropTypes.string,
|
||||||
deletable: PropTypes.bool,
|
deletable: PropTypes.bool,
|
||||||
deleted: PropTypes.bool,
|
deleted: PropTypes.bool,
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
onDelete: PropTypes.func
|
onAddComment: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
projectId: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Comment;
|
module.exports = Comment;
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
@import "../../../colors";
|
@import "../../../colors";
|
||||||
|
|
||||||
.compose-comment {
|
.compose-comment {
|
||||||
|
margin-left: .5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.compose-error-row {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.compose-error-tip {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
border: 1px solid $active-gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $ui-orange;
|
||||||
|
padding: .25rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
color: $type-white;
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.textarea-row {
|
.textarea-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -161,6 +179,9 @@
|
||||||
|
|
||||||
.comment-reply {
|
.comment-reply {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $ui-blue;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
|
@ -197,6 +218,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comments-root-reply {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-reply-row {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.expand-thread {
|
.expand-thread {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,26 +1,203 @@
|
||||||
const React = require('react');
|
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 FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||||
|
const Avatar = require('../../../components/avatar/avatar.jsx');
|
||||||
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
||||||
const Button = require('../../../components/forms/button.jsx');
|
const Button = require('../../../components/forms/button.jsx');
|
||||||
|
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
|
const api = require('../../../lib/api');
|
||||||
|
|
||||||
require('./comment.scss');
|
require('./comment.scss');
|
||||||
|
|
||||||
const onUpdate = update => update;
|
const onUpdate = update => update;
|
||||||
|
|
||||||
const ComposeComment = () => (
|
const MAX_COMMENT_LENGTH = 500;
|
||||||
|
|
||||||
|
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.',
|
||||||
|
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 {
|
||||||
|
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: 'error'};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) {
|
||||||
|
// Note: does not reset the message state
|
||||||
|
this.setState({
|
||||||
|
status: ComposeStatus.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
this.props.onAddComment(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleCancel () {
|
||||||
|
this.setState({
|
||||||
|
message: '',
|
||||||
|
status: ComposeStatus.EDITING,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
if (this.props.onCancel) this.props.onCancel();
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-row comment"
|
||||||
|
>
|
||||||
|
<a href={`/users/${this.props.user.username}`}>
|
||||||
|
<Avatar src={this.props.user.thumbnailUrl} />
|
||||||
|
</a>
|
||||||
<FlexRow className="compose-comment column">
|
<FlexRow className="compose-comment column">
|
||||||
|
{this.state.error ? (
|
||||||
|
<FlexRow className="compose-error-row">
|
||||||
|
<div className="compose-error-tip">
|
||||||
|
{CommentErrorMessages[this.state.error] || 'Unknown error'}
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
) : null}
|
||||||
<InplaceInput
|
<InplaceInput
|
||||||
|
className={classNames('compose-input',
|
||||||
|
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ? 'compose-valid' : 'compose-invalid')}
|
||||||
handleUpdate={onUpdate}
|
handleUpdate={onUpdate}
|
||||||
name="compose-comment"
|
name="compose-comment"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
value={this.state.message}
|
||||||
|
onInput={this.handleInput}
|
||||||
/>
|
/>
|
||||||
<FlexRow className="compose-bottom-row">
|
<FlexRow className="compose-bottom-row">
|
||||||
<Button className="compose-post">Post</Button>
|
<Button
|
||||||
<Button className="compose-cancel">Cancel</Button>
|
className="compose-post"
|
||||||
<span className="compose-limit">500 characters left</span>
|
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
||||||
|
onClick={this.handlePost}
|
||||||
|
>
|
||||||
|
{this.state.status === ComposeStatus.SUBMITTING ? (
|
||||||
|
'Posting...' /* TODO internationalize */
|
||||||
|
) : (
|
||||||
|
'Post' /* TODO internationalize */
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="compose-cancel"
|
||||||
|
onClick={this.handleCancel}
|
||||||
|
>
|
||||||
|
Cancel {/* TODO internationalize */}
|
||||||
|
</Button>
|
||||||
|
<span
|
||||||
|
className={classNames('compose-limit',
|
||||||
|
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||||
|
'compose-valid' : 'compose-invalid')}
|
||||||
|
>
|
||||||
|
{/* TODO internationalize */}
|
||||||
|
{MAX_COMMENT_LENGTH - this.state.message.length} characters left
|
||||||
|
</span>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = ComposeComment;
|
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;
|
||||||
|
|
|
@ -13,7 +13,8 @@ class TopLevelComment extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleExpandThread',
|
'handleExpandThread',
|
||||||
'handleDelete'
|
'handleAddComment',
|
||||||
|
'handleDeleteReply'
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
expanded: false
|
expanded: false
|
||||||
|
@ -26,26 +27,36 @@ class TopLevelComment extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDelete () {
|
handleDeleteReply (commentId) {
|
||||||
this.props.onDelete(this.props.id);
|
// 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 () {
|
render () {
|
||||||
const {
|
const {
|
||||||
author,
|
author,
|
||||||
|
canReply,
|
||||||
content,
|
content,
|
||||||
datetimeCreated,
|
datetimeCreated,
|
||||||
deletable,
|
deletable,
|
||||||
deleted,
|
deleted,
|
||||||
id,
|
id,
|
||||||
replies
|
onDelete,
|
||||||
|
replies,
|
||||||
|
projectId
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlexRow className="comment-container">
|
<FlexRow className="comment-container">
|
||||||
<Comment
|
<Comment
|
||||||
onDelete={this.handleDelete}
|
projectId={projectId}
|
||||||
{...{author, content, datetimeCreated, deletable, deleted, id}}
|
onAddComment={this.handleAddComment}
|
||||||
|
{...{author, content, datetimeCreated, deletable, deleted, canReply, id, onDelete}}
|
||||||
/>
|
/>
|
||||||
{replies.length > 0 &&
|
{replies.length > 0 &&
|
||||||
<FlexRow
|
<FlexRow
|
||||||
|
@ -59,13 +70,16 @@ class TopLevelComment extends React.Component {
|
||||||
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
|
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
|
||||||
<Comment
|
<Comment
|
||||||
author={reply.author}
|
author={reply.author}
|
||||||
|
canReply={canReply}
|
||||||
content={reply.content}
|
content={reply.content}
|
||||||
datetimeCreated={reply.datetime_created}
|
datetimeCreated={reply.datetime_created}
|
||||||
deletable={deletable}
|
deletable={deletable}
|
||||||
deleted={reply.deleted}
|
deleted={reply.deleted}
|
||||||
id={reply.id}
|
id={reply.id}
|
||||||
key={reply.id}
|
key={reply.id}
|
||||||
onDelete={this.handleDelete}
|
projectId={projectId}
|
||||||
|
onAddComment={this.handleAddComment}
|
||||||
|
onDelete={this.handleDeleteReply}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
@ -87,11 +101,13 @@ TopLevelComment.propTypes = {
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
canReply: PropTypes.bool,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
datetimeCreated: PropTypes.string,
|
datetimeCreated: PropTypes.string,
|
||||||
deletable: PropTypes.bool,
|
deletable: PropTypes.bool,
|
||||||
deleted: PropTypes.bool,
|
deleted: PropTypes.bool,
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
onAddComment: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
parentId: PropTypes.number,
|
parentId: PropTypes.number,
|
||||||
projectId: PropTypes.string,
|
projectId: PropTypes.string,
|
||||||
|
|
|
@ -21,6 +21,7 @@ const StudioList = require('./studio-list.jsx');
|
||||||
const Subactions = require('./subactions.jsx');
|
const Subactions = require('./subactions.jsx');
|
||||||
const InplaceInput = require('../../components/forms/inplace-input.jsx');
|
const InplaceInput = require('../../components/forms/inplace-input.jsx');
|
||||||
const TopLevelComment = require('./comment/top-level-comment.jsx');
|
const TopLevelComment = require('./comment/top-level-comment.jsx');
|
||||||
|
const ComposeComment = require('./comment/compose-comment.jsx');
|
||||||
const ExtensionChip = require('./extension-chip.jsx');
|
const ExtensionChip = require('./extension-chip.jsx');
|
||||||
|
|
||||||
const projectShape = require('./projectshape.jsx').projectShape;
|
const projectShape = require('./projectshape.jsx').projectShape;
|
||||||
|
@ -64,6 +65,7 @@ const PreviewPresentation = ({
|
||||||
projectStudios,
|
projectStudios,
|
||||||
studios,
|
studios,
|
||||||
userOwnsProject,
|
userOwnsProject,
|
||||||
|
onAddComment,
|
||||||
onDeleteComment,
|
onDeleteComment,
|
||||||
onFavoriteClicked,
|
onFavoriteClicked,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
|
@ -317,10 +319,21 @@ const PreviewPresentation = ({
|
||||||
<h4>Comments</h4>
|
<h4>Comments</h4>
|
||||||
{/* TODO: Add toggle comments component and logic*/}
|
{/* TODO: Add toggle comments component and logic*/}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
|
||||||
|
<FlexRow className="comments-root-reply">
|
||||||
|
{isLoggedIn &&
|
||||||
|
<ComposeComment
|
||||||
|
projectId={projectId}
|
||||||
|
onAddComment={onAddComment}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</FlexRow>
|
||||||
|
|
||||||
<FlexRow className="comments-list">
|
<FlexRow className="comments-list">
|
||||||
{comments.map(comment => (
|
{comments.map(comment => (
|
||||||
<TopLevelComment
|
<TopLevelComment
|
||||||
author={comment.author}
|
author={comment.author}
|
||||||
|
canReply={isLoggedIn}
|
||||||
content={comment.content}
|
content={comment.content}
|
||||||
datetimeCreated={comment.datetime_created}
|
datetimeCreated={comment.datetime_created}
|
||||||
deletable={userOwnsProject}
|
deletable={userOwnsProject}
|
||||||
|
@ -330,6 +343,7 @@ 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] : []}
|
||||||
|
onAddComment={onAddComment}
|
||||||
onDelete={onDeleteComment}
|
onDelete={onDeleteComment}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -374,6 +388,7 @@ PreviewPresentation.propTypes = {
|
||||||
isShared: PropTypes.bool,
|
isShared: PropTypes.bool,
|
||||||
loveCount: PropTypes.number,
|
loveCount: PropTypes.number,
|
||||||
loved: PropTypes.bool,
|
loved: PropTypes.bool,
|
||||||
|
onAddComment: PropTypes.func,
|
||||||
onAddToStudioClicked: PropTypes.func,
|
onAddToStudioClicked: PropTypes.func,
|
||||||
onAddToStudioClosed: PropTypes.func,
|
onAddToStudioClosed: PropTypes.func,
|
||||||
onDeleteComment: PropTypes.func,
|
onDeleteComment: PropTypes.func,
|
||||||
|
|
|
@ -33,6 +33,7 @@ class Preview extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'addEventListeners',
|
'addEventListeners',
|
||||||
|
'handleAddComment',
|
||||||
'handleDeleteComment',
|
'handleDeleteComment',
|
||||||
'handleToggleStudio',
|
'handleToggleStudio',
|
||||||
'handleFavoriteToggle',
|
'handleFavoriteToggle',
|
||||||
|
@ -166,8 +167,11 @@ class Preview extends React.Component {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleDeleteComment (id) {
|
handleAddComment (comment, topLevelCommentId) {
|
||||||
this.props.handleDeleteComment(this.state.projectId, id, this.props.user.token);
|
this.props.handleAddComment(comment, topLevelCommentId);
|
||||||
|
}
|
||||||
|
handleDeleteComment (id, topLevelCommentId) {
|
||||||
|
this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
|
||||||
}
|
}
|
||||||
handleReportClick () {
|
handleReportClick () {
|
||||||
this.setState({reportOpen: true});
|
this.setState({reportOpen: true});
|
||||||
|
@ -341,6 +345,7 @@ class Preview extends React.Component {
|
||||||
reportOpen={this.state.reportOpen}
|
reportOpen={this.state.reportOpen}
|
||||||
studios={this.props.studios}
|
studios={this.props.studios}
|
||||||
userOwnsProject={this.props.userOwnsProject}
|
userOwnsProject={this.props.userOwnsProject}
|
||||||
|
onAddComment={this.handleAddComment}
|
||||||
onAddToStudioClicked={this.handleAddToStudioClick}
|
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||||
onAddToStudioClosed={this.handleAddToStudioClose}
|
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||||
onDeleteComment={this.handleDeleteComment}
|
onDeleteComment={this.handleDeleteComment}
|
||||||
|
@ -399,6 +404,7 @@ Preview.propTypes = {
|
||||||
getProjectStudios: PropTypes.func.isRequired,
|
getProjectStudios: PropTypes.func.isRequired,
|
||||||
getRemixes: PropTypes.func.isRequired,
|
getRemixes: PropTypes.func.isRequired,
|
||||||
getTopLevelComments: PropTypes.func.isRequired,
|
getTopLevelComments: PropTypes.func.isRequired,
|
||||||
|
handleAddComment: PropTypes.func,
|
||||||
handleDeleteComment: PropTypes.func,
|
handleDeleteComment: PropTypes.func,
|
||||||
handleLogIn: PropTypes.func,
|
handleLogIn: PropTypes.func,
|
||||||
handleLogOut: PropTypes.func,
|
handleLogOut: PropTypes.func,
|
||||||
|
@ -526,8 +532,11 @@ const mapStateToProps = state => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
handleDeleteComment: (projectId, commentId, token) => {
|
handleAddComment: (comment, topLevelCommentId) => {
|
||||||
dispatch(previewActions.deleteComment(projectId, commentId, token));
|
dispatch(previewActions.addNewComment(comment, topLevelCommentId));
|
||||||
|
},
|
||||||
|
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
|
||||||
|
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
|
||||||
},
|
},
|
||||||
handleOpenRegistration: event => {
|
handleOpenRegistration: event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
Loading…
Reference in a new issue