mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 15:47:53 -05:00
Merge branch 'develop' into studio-selectors
This commit is contained in:
commit
dc460fffde
28 changed files with 1509 additions and 1016 deletions
|
@ -81,7 +81,7 @@ aliases:
|
|||
- run:
|
||||
name: "setup python"
|
||||
command: |
|
||||
curl https://bootstrap.pypa.io/3.5/get-pip.py -o get-pip.py
|
||||
curl https://bootstrap.pypa.io/pip/3.5/get-pip.py -o get-pip.py
|
||||
python3 get-pip.py pip==21.0.1
|
||||
pip install s3cmd==2.1.0
|
||||
- run:
|
||||
|
@ -134,7 +134,7 @@ jobs:
|
|||
# <<: *integration_tap
|
||||
|
||||
workflows:
|
||||
build-staging-production: # build-test-deploy
|
||||
build-test-deploy:
|
||||
jobs:
|
||||
- build-staging:
|
||||
context:
|
||||
|
@ -154,42 +154,39 @@ workflows:
|
|||
branches:
|
||||
only:
|
||||
- master
|
||||
# - deploy-staging:
|
||||
# context:
|
||||
# - scratch-www-all
|
||||
# - scratch-www-staging
|
||||
# requires:
|
||||
# - build-staging
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - develop
|
||||
# - /^hotfix\/.*/
|
||||
# - /^release\/.*/
|
||||
# - circleCI-configure-tests
|
||||
# - integration-staging-jest:
|
||||
# context:
|
||||
# - scratch-www-all
|
||||
# - scratch-www-staging
|
||||
# requires:
|
||||
# - deploy-staging
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - develop
|
||||
# - /^hotfix\/.*/
|
||||
# - /^release\/.*/
|
||||
# - circleCI-configure-tests
|
||||
# - integration-staging-tap:
|
||||
# context:
|
||||
# - scratch-www-all
|
||||
# - scratch-www-staging
|
||||
# requires:
|
||||
# - deploy-staging
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - develop
|
||||
# - /^hotfix\/.*/
|
||||
# - /^release\/.*/
|
||||
# - circleCI-configure-tests
|
||||
- deploy-staging:
|
||||
context:
|
||||
- scratch-www-all
|
||||
- scratch-www-staging
|
||||
requires:
|
||||
- build-staging
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- /^hotfix\/.*/
|
||||
- /^release\/.*/
|
||||
- integration-staging-jest:
|
||||
context:
|
||||
- scratch-www-all
|
||||
- scratch-www-staging
|
||||
requires:
|
||||
- deploy-staging
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- /^hotfix\/.*/
|
||||
- /^release\/.*/
|
||||
- integration-staging-tap:
|
||||
context:
|
||||
- scratch-www-all
|
||||
- scratch-www-staging
|
||||
requires:
|
||||
- deploy-staging
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- /^hotfix\/.*/
|
||||
- /^release\/.*/
|
||||
|
|
11
.travis.yml
11
.travis.yml
|
@ -114,15 +114,6 @@ jobs:
|
|||
include:
|
||||
- stage: test
|
||||
deploy:
|
||||
- provider: script
|
||||
skip_cleanup: $SKIP_CLEANUP
|
||||
script: npm run deploy
|
||||
on:
|
||||
repo: LLK/scratch-www
|
||||
branch:
|
||||
- develop
|
||||
- hotfix/*
|
||||
- release/*
|
||||
- provider: script
|
||||
skip_cleanup: $SKIP_CLEANUP
|
||||
script: npm run deploy
|
||||
|
@ -138,6 +129,6 @@ stages:
|
|||
- name: test
|
||||
if: type != cron
|
||||
- name: smoke
|
||||
if: type NOT IN (cron, pull_request) AND (branch =~ /^(develop|master|release\/|hotfix\/)/)
|
||||
if: type NOT IN (cron, pull_request) AND (branch =~ /^(master)/)
|
||||
- name: update translations
|
||||
if: branch == develop AND type == cron
|
||||
|
|
842
package-lock.json
generated
842
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -19,9 +19,9 @@
|
|||
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
|
||||
"test:unit:jest:unit": "jest ./test/unit/ --reporters=default",
|
||||
"test:unit:jest:localization": "jest ./test/localization/*.test.js --reporters=default",
|
||||
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/*.js --no-coverage -R classic",
|
||||
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/ --no-coverage -R classic",
|
||||
"test:unit:convertReportToXunit": "tap ./test/results/unit-raw.tap --no-coverage -R xunit > ./test/results/unit-tap-results.xml",
|
||||
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/*.js --coverage --coverage-report=lcov",
|
||||
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/ --coverage --coverage-report=lcov",
|
||||
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
|
||||
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
|
||||
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
|
||||
|
@ -126,7 +126,7 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20210322040541",
|
||||
"scratch-gui": "0.1.0-prerelease.20210401231322",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
|
|
@ -4,47 +4,53 @@
|
|||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": "",
|
||||
"href": "#",
|
||||
"stats": {"loves": 0, "remixes": 0}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -2,129 +2,145 @@
|
|||
{
|
||||
"id": 1,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "project",
|
||||
"title": "Project",
|
||||
"title": "Project Title",
|
||||
"thumbnailUrl": "",
|
||||
"creator": "",
|
||||
"href": "#"
|
||||
"author": {"username": "project creator"},
|
||||
"href": "#",
|
||||
"stats": {}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -327,8 +327,6 @@
|
|||
"comments.isBad": "Hmm...the bad word detector thinks there is a problem with your comment. Please change it and remember to be respectful.",
|
||||
"comments.hasChatSite": "Uh oh! The comment contains a link to a website with unmoderated chat. For safety reasons, please do not link to these sites!",
|
||||
"comments.isSpam": "Hmm, seems like you've posted the same comment a bunch of times. Please don't spam.",
|
||||
"comments.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. :/",
|
||||
"comments.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.",
|
||||
"comments.isDisallowed": "Hmm, it looks like comments have been turned off for this page. :/",
|
||||
"comments.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. If you'd like to appeal this block, you can contact appeals@scratch.mit.edu and reference Case Number {appealId}.",
|
||||
"comments.isTooLong": "That comment is too long! Please find a way to shorten your text.",
|
||||
|
|
188
src/redux/comments.js
Normal file
188
src/redux/comments.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
const keyMirror = require('keymirror');
|
||||
const mergeWith = require('lodash.mergewith');
|
||||
const uniqBy = require('lodash.uniqby');
|
||||
|
||||
const COMMENT_LIMIT = 20;
|
||||
|
||||
module.exports.Status = keyMirror({
|
||||
FETCHED: null,
|
||||
NOT_FETCHED: null,
|
||||
FETCHING: null,
|
||||
ERROR: null
|
||||
});
|
||||
|
||||
module.exports.getInitialState = () => ({
|
||||
status: {
|
||||
comments: module.exports.Status.NOT_FETCHED
|
||||
},
|
||||
comments: [],
|
||||
replies: {},
|
||||
moreCommentsToLoad: false
|
||||
});
|
||||
|
||||
module.exports.commentsReducer = (state, action) => {
|
||||
if (typeof state === 'undefined') {
|
||||
state = module.exports.getInitialState();
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'RESET_TO_INTIAL_STATE':
|
||||
return module.exports.getInitialState();
|
||||
case 'RESET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
comments: [],
|
||||
replies: {}
|
||||
});
|
||||
case 'SET_COMMENT_FETCH_STATUS':
|
||||
return Object.assign({}, state, {
|
||||
status: Object.assign({}, state.status, {
|
||||
[action.infoType]: action.status
|
||||
})
|
||||
});
|
||||
case 'SET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
comments: uniqBy(state.comments.concat(action.items), 'id')
|
||||
});
|
||||
case 'UPDATE_COMMENT':
|
||||
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, action.comment);
|
||||
}
|
||||
return comment;
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
comments: state.comments.map(comment => {
|
||||
if (comment.id === action.commentId) {
|
||||
return Object.assign({}, comment, action.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 'UPDATE_ALL_REPLIES':
|
||||
return Object.assign({}, state, {
|
||||
replies: Object.assign({}, state.replies, {
|
||||
[action.commentId]: state.replies[action.commentId].map(reply =>
|
||||
Object.assign({}, reply, action.comment)
|
||||
)
|
||||
})
|
||||
});
|
||||
case 'SET_REPLIES':
|
||||
return Object.assign({}, state, {
|
||||
// Append new replies to the state.replies structure
|
||||
replies: mergeWith({}, state.replies, action.replies, (replies, newReplies) => (
|
||||
uniqBy((replies || []).concat(newReplies || []), 'id')
|
||||
)),
|
||||
// Also set the `moreRepliesToLoad` property on the top-level comments
|
||||
comments: state.comments.map(comment => {
|
||||
if (action.replies[comment.id]) {
|
||||
return Object.assign({}, comment, {
|
||||
moreRepliesToLoad: action.replies[comment.id].length === COMMENT_LIMIT
|
||||
});
|
||||
}
|
||||
return comment;
|
||||
})
|
||||
});
|
||||
case 'SET_MORE_COMMENTS_TO_LOAD':
|
||||
return Object.assign({}, state, {
|
||||
moreCommentsToLoad: action.moreCommentsToLoad
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setFetchStatus = (type, status) => ({
|
||||
type: 'SET_COMMENT_FETCH_STATUS',
|
||||
infoType: type,
|
||||
status: status
|
||||
});
|
||||
|
||||
module.exports.setComments = items => ({
|
||||
type: 'SET_COMMENTS',
|
||||
items: items
|
||||
});
|
||||
|
||||
module.exports.setReplies = replies => ({
|
||||
type: 'SET_REPLIES',
|
||||
replies: replies
|
||||
});
|
||||
|
||||
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'deleted'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setRepliesDeleted = commentId => ({
|
||||
type: 'UPDATE_ALL_REPLIES',
|
||||
commentId: commentId,
|
||||
comment: {
|
||||
visibility: 'deleted'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'reported'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'visible'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setRepliesRestored = commentId => ({
|
||||
type: 'UPDATE_ALL_REPLIES',
|
||||
commentId: commentId,
|
||||
comment: {
|
||||
visibility: 'visible'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.addNewComment = (comment, topLevelCommentId) => ({
|
||||
type: 'ADD_NEW_COMMENT',
|
||||
comment: comment,
|
||||
topLevelCommentId: topLevelCommentId
|
||||
});
|
||||
|
||||
module.exports.setMoreCommentsToLoad = moreCommentsToLoad => ({
|
||||
type: 'SET_MORE_COMMENTS_TO_LOAD',
|
||||
moreCommentsToLoad: moreCommentsToLoad
|
||||
});
|
||||
|
||||
module.exports.resetComments = () => ({
|
||||
type: 'RESET_COMMENTS'
|
||||
});
|
|
@ -1,14 +1,9 @@
|
|||
const defaults = require('lodash.defaults');
|
||||
const keyMirror = require('keymirror');
|
||||
const eachLimit = require('async/eachLimit');
|
||||
const mergeWith = require('lodash.mergewith');
|
||||
const uniqBy = require('lodash.uniqby');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log');
|
||||
|
||||
const COMMENT_LIMIT = 20;
|
||||
|
||||
module.exports.Status = keyMirror({
|
||||
FETCHED: null,
|
||||
NOT_FETCHED: null,
|
||||
|
@ -19,7 +14,6 @@ module.exports.Status = keyMirror({
|
|||
module.exports.getInitialState = () => ({
|
||||
status: {
|
||||
project: module.exports.Status.NOT_FETCHED,
|
||||
comments: module.exports.Status.NOT_FETCHED,
|
||||
faved: module.exports.Status.NOT_FETCHED,
|
||||
loved: module.exports.Status.NOT_FETCHED,
|
||||
original: module.exports.Status.NOT_FETCHED,
|
||||
|
@ -33,8 +27,6 @@ module.exports.getInitialState = () => ({
|
|||
},
|
||||
projectInfo: {},
|
||||
remixes: [],
|
||||
comments: [],
|
||||
replies: {},
|
||||
faved: false,
|
||||
loved: false,
|
||||
original: {},
|
||||
|
@ -42,7 +34,6 @@ module.exports.getInitialState = () => ({
|
|||
projectStudios: [],
|
||||
curatedStudios: [],
|
||||
currentStudioIds: [],
|
||||
moreCommentsToLoad: false,
|
||||
projectNotAvailable: false,
|
||||
visibilityInfo: {}
|
||||
});
|
||||
|
@ -96,76 +87,6 @@ module.exports.previewReducer = (state, action) => {
|
|||
item !== action.studioId
|
||||
))
|
||||
});
|
||||
case 'RESET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
comments: [],
|
||||
replies: {}
|
||||
});
|
||||
case 'SET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
comments: uniqBy(state.comments.concat(action.items), 'id')
|
||||
});
|
||||
case 'UPDATE_COMMENT':
|
||||
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, action.comment);
|
||||
}
|
||||
return comment;
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
comments: state.comments.map(comment => {
|
||||
if (comment.id === action.commentId) {
|
||||
return Object.assign({}, comment, action.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 'UPDATE_ALL_REPLIES':
|
||||
return Object.assign({}, state, {
|
||||
replies: Object.assign({}, state.replies, {
|
||||
[action.commentId]: state.replies[action.commentId].map(reply =>
|
||||
Object.assign({}, reply, action.comment)
|
||||
)
|
||||
})
|
||||
});
|
||||
case 'SET_REPLIES':
|
||||
return Object.assign({}, state, {
|
||||
// Append new replies to the state.replies structure
|
||||
replies: mergeWith({}, state.replies, action.replies, (replies, newReplies) => (
|
||||
uniqBy((replies || []).concat(newReplies || []), 'id')
|
||||
)),
|
||||
// Also set the `moreRepliesToLoad` property on the top-level comments
|
||||
comments: state.comments.map(comment => {
|
||||
if (action.replies[comment.id]) {
|
||||
return Object.assign({}, comment, {
|
||||
moreRepliesToLoad: action.replies[comment.id].length === COMMENT_LIMIT
|
||||
});
|
||||
}
|
||||
return comment;
|
||||
})
|
||||
});
|
||||
case 'SET_LOVED':
|
||||
return Object.assign({}, state, {
|
||||
loved: action.info
|
||||
|
@ -182,10 +103,6 @@ module.exports.previewReducer = (state, action) => {
|
|||
state = JSON.parse(JSON.stringify(state));
|
||||
state.status.studioRequests[action.studioId] = action.status;
|
||||
return state;
|
||||
case 'SET_MORE_COMMENTS_TO_LOAD':
|
||||
return Object.assign({}, state, {
|
||||
moreCommentsToLoad: action.moreCommentsToLoad
|
||||
});
|
||||
case 'SET_VISIBILITY_INFO':
|
||||
return Object.assign({}, state, {
|
||||
visibilityInfo: action.visibilityInfo
|
||||
|
@ -247,16 +164,6 @@ module.exports.setProjectStudios = items => ({
|
|||
items: items
|
||||
});
|
||||
|
||||
module.exports.setComments = items => ({
|
||||
type: 'SET_COMMENTS',
|
||||
items: items
|
||||
});
|
||||
|
||||
module.exports.setReplies = replies => ({
|
||||
type: 'SET_REPLIES',
|
||||
replies: replies
|
||||
});
|
||||
|
||||
module.exports.setCuratedStudios = items => ({
|
||||
type: 'SET_CURATED_STUDIOS',
|
||||
items: items
|
||||
|
@ -284,64 +191,6 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
|
|||
status: status
|
||||
});
|
||||
|
||||
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'deleted'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setRepliesDeleted = commentId => ({
|
||||
type: 'UPDATE_ALL_REPLIES',
|
||||
commentId: commentId,
|
||||
comment: {
|
||||
visibility: 'deleted'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'reported'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
|
||||
type: 'UPDATE_COMMENT',
|
||||
commentId: commentId,
|
||||
topLevelCommentId: topLevelCommentId,
|
||||
comment: {
|
||||
visibility: 'visible'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.setRepliesRestored = commentId => ({
|
||||
type: 'UPDATE_ALL_REPLIES',
|
||||
commentId: commentId,
|
||||
comment: {
|
||||
visibility: 'visible'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.addNewComment = (comment, topLevelCommentId) => ({
|
||||
type: 'ADD_NEW_COMMENT',
|
||||
comment: comment,
|
||||
topLevelCommentId: topLevelCommentId
|
||||
});
|
||||
|
||||
module.exports.setMoreCommentsToLoad = moreCommentsToLoad => ({
|
||||
type: 'SET_MORE_COMMENTS_TO_LOAD',
|
||||
moreCommentsToLoad: moreCommentsToLoad
|
||||
});
|
||||
|
||||
module.exports.resetComments = () => ({
|
||||
type: 'RESET_COMMENTS'
|
||||
});
|
||||
|
||||
module.exports.setVisibilityInfo = visibilityInfo => ({
|
||||
type: 'SET_VISIBILITY_INFO',
|
||||
visibilityInfo: visibilityInfo
|
||||
|
@ -462,94 +311,6 @@ module.exports.getFavedStatus = (id, username, token) => (dispatch => {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports.getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${id}/comments`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setComments(body));
|
||||
dispatch(module.exports.getReplies(id, body.map(comment => comment.id), 0, ownerUsername, isAdmin, token));
|
||||
|
||||
// If we loaded a full page of comments, assume there are more to load.
|
||||
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
|
||||
// any more server query complexity, so seems worth it. In the case of a project with
|
||||
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
|
||||
// clickable, but upon clicking it will go away.
|
||||
dispatch(module.exports.setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${commentId}`,
|
||||
authentication: token ? token : null
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (!body || res.statusCode >= 400) { // NotFound
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.parent_id) {
|
||||
// If the comment is a reply, load the parent
|
||||
return dispatch(module.exports.getCommentById(projectId, body.parent_id, ownerUsername, isAdmin, token));
|
||||
}
|
||||
|
||||
// If the comment is not a reply, show it as top level and load replies
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setComments([body]));
|
||||
dispatch(module.exports.getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
|
||||
const fetchedReplies = {};
|
||||
eachLimit(commentIds, 10, (parentId, callback) => {
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
return callback(`Error fetching comment replies: ${err}`);
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
return callback('No comment reply information');
|
||||
}
|
||||
fetchedReplies[parentId] = body;
|
||||
callback(null, body);
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setReplies(fetchedReplies));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
|
||||
if (faved) {
|
||||
|
@ -882,62 +643,6 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports.deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
/* TODO fetching/fetched/error states updates for comment deleting */
|
||||
api({
|
||||
uri: `/proxy/comments/project/${projectId}/comment/${commentId}`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'DELETE',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setCommentDeleted(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(module.exports.setRepliesDeleted(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
api({
|
||||
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setCommentRestored(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(module.exports.setRepliesRestored(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.shareProject = (projectId, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
|
||||
api({
|
||||
|
|
180
src/redux/project-comment-actions.js
Normal file
180
src/redux/project-comment-actions.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
const eachLimit = require('async/eachLimit');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log');
|
||||
|
||||
const COMMENT_LIMIT = 20;
|
||||
|
||||
const {
|
||||
addNewComment,
|
||||
resetComments,
|
||||
Status,
|
||||
setFetchStatus,
|
||||
setCommentDeleted,
|
||||
setCommentReported,
|
||||
setCommentRestored,
|
||||
setMoreCommentsToLoad,
|
||||
setComments,
|
||||
setError,
|
||||
setReplies,
|
||||
setRepliesDeleted,
|
||||
setRepliesRestored
|
||||
} = require('../redux/comments.js');
|
||||
|
||||
const getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('replies', Status.FETCHING));
|
||||
const fetchedReplies = {};
|
||||
eachLimit(commentIds, 10, (parentId, callback) => {
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
return callback(`Error fetching comment replies: ${err}`);
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
return callback('No comment reply information');
|
||||
}
|
||||
fetchedReplies[parentId] = body;
|
||||
callback(null, body);
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('replies', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('replies', Status.FETCHED));
|
||||
dispatch(setReplies(fetchedReplies));
|
||||
});
|
||||
});
|
||||
|
||||
const getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('comments', Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${id}/comments`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('comments', Status.FETCHED));
|
||||
dispatch(setComments(body));
|
||||
dispatch(getReplies(id, body.map(comment => comment.id), 0, ownerUsername, isAdmin, token));
|
||||
|
||||
// If we loaded a full page of comments, assume there are more to load.
|
||||
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
|
||||
// any more server query complexity, so seems worth it. In the case of a project with
|
||||
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
|
||||
// clickable, but upon clicking it will go away.
|
||||
dispatch(setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
|
||||
});
|
||||
});
|
||||
|
||||
const getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('comments', Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${commentId}`,
|
||||
authentication: token ? token : null
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
if (!body || res.statusCode >= 400) { // NotFound
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.parent_id) {
|
||||
// If the comment is a reply, load the parent
|
||||
return dispatch(getCommentById(projectId, body.parent_id, ownerUsername, isAdmin, token));
|
||||
}
|
||||
|
||||
// If the comment is not a reply, show it as top level and load replies
|
||||
dispatch(setFetchStatus('comments', Status.FETCHED));
|
||||
dispatch(setComments([body]));
|
||||
dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
|
||||
});
|
||||
});
|
||||
|
||||
const deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
/* TODO fetching/fetched/error states updates for comment deleting */
|
||||
api({
|
||||
uri: `/proxy/comments/project/${projectId}/comment/${commentId}`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'DELETE',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(setCommentDeleted(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(setRepliesDeleted(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const 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(setCommentReported(commentId, topLevelCommentId));
|
||||
});
|
||||
});
|
||||
|
||||
const restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
api({
|
||||
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(setCommentRestored(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(setRepliesRestored(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getTopLevelComments,
|
||||
getCommentById,
|
||||
getReplies,
|
||||
deleteComment,
|
||||
reportComment,
|
||||
restoreComment,
|
||||
|
||||
// Re-export these specific action creators directly so the implementer
|
||||
// does not need to go to two places for comment actions
|
||||
addNewComment,
|
||||
resetComments
|
||||
};
|
180
src/redux/studio-comment-actions.js
Normal file
180
src/redux/studio-comment-actions.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
const eachLimit = require('async/eachLimit');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log');
|
||||
|
||||
const COMMENT_LIMIT = 20;
|
||||
|
||||
const {
|
||||
addNewComment,
|
||||
resetComments,
|
||||
Status,
|
||||
setFetchStatus,
|
||||
setCommentDeleted,
|
||||
setCommentReported,
|
||||
setCommentRestored,
|
||||
setMoreCommentsToLoad,
|
||||
setComments,
|
||||
setError,
|
||||
setReplies,
|
||||
setRepliesDeleted,
|
||||
setRepliesRestored
|
||||
} = require('../redux/comments.js');
|
||||
|
||||
const getReplies = (studioId, commentIds, offset, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('replies', Status.FETCHING));
|
||||
const fetchedReplies = {};
|
||||
eachLimit(commentIds, 10, (parentId, callback) => {
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${parentId}/replies`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
return callback(`Error fetching comment replies: ${err}`);
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
return callback('No comment reply information');
|
||||
}
|
||||
fetchedReplies[parentId] = body;
|
||||
callback(null, body);
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('replies', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('replies', Status.FETCHED));
|
||||
dispatch(setReplies(fetchedReplies));
|
||||
});
|
||||
});
|
||||
|
||||
const getTopLevelComments = (id, offset, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('comments', Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : ''}/studios/${id}/comments`,
|
||||
authentication: token ? token : null,
|
||||
params: {offset: offset || 0, limit: COMMENT_LIMIT}
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('comments', Status.FETCHED));
|
||||
dispatch(setComments(body));
|
||||
dispatch(getReplies(id, body.map(comment => comment.id), 0, isAdmin, token));
|
||||
|
||||
// If we loaded a full page of comments, assume there are more to load.
|
||||
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
|
||||
// any more server query complexity, so seems worth it. In the case of a project with
|
||||
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
|
||||
// clickable, but upon clicking it will go away.
|
||||
dispatch(setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
|
||||
});
|
||||
});
|
||||
|
||||
const getCommentById = (studioId, commentId, isAdmin, token) => (dispatch => {
|
||||
dispatch(setFetchStatus('comments', Status.FETCHING));
|
||||
api({
|
||||
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${commentId}`,
|
||||
authentication: token ? token : null
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError(err));
|
||||
return;
|
||||
}
|
||||
if (!body || res.statusCode >= 400) { // NotFound
|
||||
dispatch(setFetchStatus('comments', Status.ERROR));
|
||||
dispatch(setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.parent_id) {
|
||||
// If the comment is a reply, load the parent
|
||||
return dispatch(getCommentById(studioId, body.parent_id, isAdmin, token));
|
||||
}
|
||||
|
||||
// If the comment is not a reply, show it as top level and load replies
|
||||
dispatch(setFetchStatus('comments', Status.FETCHED));
|
||||
dispatch(setComments([body]));
|
||||
dispatch(getReplies(studioId, [body.id], 0, isAdmin, token));
|
||||
});
|
||||
});
|
||||
|
||||
const deleteComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
/* TODO fetching/fetched/error states updates for comment deleting */
|
||||
api({
|
||||
uri: `/proxy/comments/studio/${studioId}/comment/${commentId}`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'DELETE',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(setCommentDeleted(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(setRepliesDeleted(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const reportComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
api({
|
||||
uri: `/proxy/studio/${studioId}/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(setCommentReported(commentId, topLevelCommentId));
|
||||
});
|
||||
});
|
||||
|
||||
const restoreComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||
api({
|
||||
uri: `/proxy/admin/studio/${studioId}/comment/${commentId}/undelete`,
|
||||
authentication: token,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
log.error(err || res.body);
|
||||
return;
|
||||
}
|
||||
dispatch(setCommentRestored(commentId, topLevelCommentId));
|
||||
if (!topLevelCommentId) {
|
||||
dispatch(setRepliesRestored(commentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getTopLevelComments,
|
||||
getCommentById,
|
||||
getReplies,
|
||||
deleteComment,
|
||||
reportComment,
|
||||
restoreComment,
|
||||
|
||||
// Re-export these specific action creators directly so the implementer
|
||||
// does not need to go to two places for comment actions
|
||||
addNewComment,
|
||||
resetComments
|
||||
};
|
|
@ -4,12 +4,5 @@
|
|||
"pattern": "^/components/?$",
|
||||
"view": "components/components",
|
||||
"title": "Components"
|
||||
},
|
||||
{
|
||||
"name": "studio",
|
||||
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
|
||||
"routeAlias": "/studios-playground/?$",
|
||||
"view": "studio/studio",
|
||||
"title": "Studio Playground"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -296,6 +296,13 @@
|
|||
"view": "studentregistration/studentregistration",
|
||||
"title": "Class Registration"
|
||||
},
|
||||
{
|
||||
"name": "studio",
|
||||
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
|
||||
"routeAlias": "/studios-playground/?$",
|
||||
"view": "studio/studio",
|
||||
"title": "Studio Playground"
|
||||
},
|
||||
{
|
||||
"name": "teacher-faq",
|
||||
"pattern": "^/educators/faq/?$",
|
||||
|
|
|
@ -5,22 +5,81 @@ const Page = require('../../components/page/www/page.jsx');
|
|||
const Box = require('../../components/box/box.jsx');
|
||||
const Button = require('../../components/forms/button.jsx');
|
||||
const Carousel = require('../../components/carousel/carousel.jsx');
|
||||
const Form = require('../../components/forms/form.jsx');
|
||||
const Input = require('../../components/forms/input.jsx');
|
||||
const Spinner = require('../../components/spinner/spinner.jsx');
|
||||
const Grid = require('../../components/grid/grid.jsx');
|
||||
const TextArea = require('../../components/forms/textarea.jsx');
|
||||
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
|
||||
const Select = require('../../components/forms/select.jsx');
|
||||
|
||||
require('./components.scss');
|
||||
|
||||
const Components = () => (
|
||||
<div className="components">
|
||||
<div className="inner">
|
||||
<h1>Button</h1>
|
||||
<Button>I love button</Button>
|
||||
<h1>Form</h1>
|
||||
<Input
|
||||
maxLength="30"
|
||||
name="test"
|
||||
type="text"
|
||||
<h1>Nav Bubbles</h1>
|
||||
<div className="subnavigation">
|
||||
<SubNavigation>
|
||||
<a href="">
|
||||
<li className="active">
|
||||
cats
|
||||
</li>
|
||||
</a>
|
||||
<a href="">
|
||||
<li>
|
||||
also cats
|
||||
</li>
|
||||
</a>
|
||||
<a href="">
|
||||
<li>
|
||||
not cats
|
||||
</li>
|
||||
</a>
|
||||
</SubNavigation>
|
||||
</div>
|
||||
<h1>Grid</h1>
|
||||
<Grid
|
||||
showAvatar
|
||||
/>
|
||||
<h1>Button</h1>
|
||||
<Button>I love buttons</Button>
|
||||
<h1>Form</h1>
|
||||
<div className="form">
|
||||
<Form>
|
||||
<Select
|
||||
label="Drop-down"
|
||||
required
|
||||
options={[
|
||||
{
|
||||
label: 'first option',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: 'second option',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: 'third option',
|
||||
value: 3
|
||||
}
|
||||
]}
|
||||
name="name"
|
||||
value={1}
|
||||
/>
|
||||
<Input
|
||||
label="Text input"
|
||||
required
|
||||
maxLength="30"
|
||||
name="test"
|
||||
/>
|
||||
<TextArea
|
||||
label="Text area"
|
||||
name="textarea1"
|
||||
required
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<h1>Box Component</h1>
|
||||
<Box
|
||||
more="Cat Gifs"
|
||||
|
|
|
@ -5,6 +5,19 @@
|
|||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.subnavigation {
|
||||
li {
|
||||
background-color: $active-gray;
|
||||
&.active {
|
||||
background-color: $ui-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.colors {
|
||||
span {
|
||||
display: inline-block;
|
||||
|
|
|
@ -51,13 +51,6 @@ const ConferenceSplash = () => (
|
|||
value={new Date(2021, 6, 22)}
|
||||
year="numeric"
|
||||
/>
|
||||
{' - '}
|
||||
<FormattedDate
|
||||
day="2-digit"
|
||||
month="long"
|
||||
value={new Date(2021, 6, 24)}
|
||||
year="numeric"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="conf2020-panel-row">
|
||||
|
|
|
@ -110,7 +110,7 @@ class Comment extends React.Component {
|
|||
highlighted,
|
||||
id,
|
||||
parentId,
|
||||
projectId,
|
||||
postURI,
|
||||
replyUsername,
|
||||
visibility
|
||||
} = this.props;
|
||||
|
@ -234,7 +234,7 @@ class Comment extends React.Component {
|
|||
isReply
|
||||
commenteeId={author.id}
|
||||
parentId={parentId || id}
|
||||
projectId={projectId}
|
||||
postURI={postURI}
|
||||
onAddComment={this.handlePostReply}
|
||||
onCancel={this.handleToggleReplying}
|
||||
/>
|
||||
|
@ -285,7 +285,7 @@ Comment.propTypes = {
|
|||
onReport: PropTypes.func,
|
||||
onRestore: PropTypes.func,
|
||||
parentId: PropTypes.number,
|
||||
projectId: PropTypes.string,
|
||||
postURI: PropTypes.string,
|
||||
replyUsername: PropTypes.string,
|
||||
visibility: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@ class ComposeComment extends React.Component {
|
|||
handlePost () {
|
||||
this.setState({status: ComposeStatus.SUBMITTING});
|
||||
api({
|
||||
uri: `/proxy/comments/project/${this.props.projectId}`,
|
||||
uri: this.props.postURI,
|
||||
authentication: this.props.user.token,
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
|
@ -224,42 +224,43 @@ class ComposeComment extends React.Component {
|
|||
MuteModal.steps.MUTE_INFO : MuteModal.steps.COMMENT_ISSUE;
|
||||
}
|
||||
|
||||
getMuteMessageInfo () {
|
||||
getMuteMessageInfo (justMuted) {
|
||||
// return the ids for the messages that are shown for this mute type
|
||||
// If mute modals have more than one unique "step" we could pass an array of steps
|
||||
const messageInfo = {
|
||||
pii: {
|
||||
name: 'pii',
|
||||
commentType: 'comment.type.pii',
|
||||
commentTypePast: 'comment.type.pii.past',
|
||||
commentType: justMuted ? 'comment.type.pii' : 'comment.type.pii.past',
|
||||
muteStepHeader: 'comment.pii.header',
|
||||
muteStepContent: ['comment.pii.content1', 'comment.pii.content2', 'comment.pii.content3']
|
||||
},
|
||||
unconstructive: {
|
||||
name: 'unconstructive',
|
||||
commentType: 'comment.type.unconstructive',
|
||||
commentTypePast: 'comment.type.unconstructive.past',
|
||||
commentType: justMuted ? 'comment.type.unconstructive' : 'comment.type.unconstructive.past',
|
||||
muteStepHeader: 'comment.unconstructive.header',
|
||||
muteStepContent: ['comment.unconstructive.content1', 'comment.unconstructive.content2']
|
||||
muteStepContent: [
|
||||
justMuted ? 'comment.unconstructive.content1' : 'comment.type.unconstructive.past',
|
||||
'comment.unconstructive.content2'
|
||||
]
|
||||
},
|
||||
vulgarity: {
|
||||
name: 'vulgarity',
|
||||
commentType: 'comment.type.vulgarity',
|
||||
commentTypePast: 'comment.type.vulgarity.past',
|
||||
commentType: justMuted ? 'comment.type.vulgarity' : 'comment.type.vulgarity.past',
|
||||
muteStepHeader: 'comment.vulgarity.header',
|
||||
muteStepContent: ['comment.vulgarity.content1', 'comment.vulgarity.content2']
|
||||
muteStepContent: [
|
||||
justMuted ? 'comment.vulgarity.content1' : 'comment.type.vulgarity.past',
|
||||
'comment.vulgarity.content2'
|
||||
]
|
||||
},
|
||||
spam: {
|
||||
name: 'spam',
|
||||
commentType: 'comment.type.spam',
|
||||
commentTypePast: 'comment.type.spam.past',
|
||||
commentType: justMuted ? 'comment.type.spam' : 'comment.type.spam.past',
|
||||
muteStepHeader: 'comment.spam.header',
|
||||
muteStepContent: ['comment.spam.content1', 'comment.spam.content2']
|
||||
},
|
||||
general: {
|
||||
name: 'general',
|
||||
commentType: 'comment.type.general',
|
||||
commentTypePast: 'comment.type.general.past',
|
||||
commentType: justMuted ? 'comment.type.general' : 'comment.type.general.past',
|
||||
muteStepHeader: 'comment.general.header',
|
||||
muteStepContent: ['comment.general.content1']
|
||||
}
|
||||
|
@ -292,9 +293,9 @@ class ComposeComment extends React.Component {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id={
|
||||
this.state.status === ComposeStatus.REJECTED_MUTE ?
|
||||
this.getMuteMessageInfo().commentType :
|
||||
this.getMuteMessageInfo().commentTypePast
|
||||
this.getMuteMessageInfo(
|
||||
this.state.status === ComposeStatus.REJECTED_MUTE
|
||||
).commentType
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
|
@ -404,7 +405,7 @@ class ComposeComment extends React.Component {
|
|||
useStandardSizes
|
||||
className="mod-mute"
|
||||
commentContent={this.state.message}
|
||||
muteModalMessages={this.getMuteMessageInfo()}
|
||||
muteModalMessages={this.getMuteMessageInfo(this.state.status === ComposeStatus.REJECTED_MUTE)}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
showFeedback={
|
||||
this.state.status === ComposeStatus.REJECTED_MUTE
|
||||
|
@ -433,7 +434,7 @@ ComposeComment.propTypes = {
|
|||
onAddComment: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
parentId: PropTypes.number,
|
||||
projectId: PropTypes.string,
|
||||
postURI: PropTypes.string,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
|
|
|
@ -87,7 +87,7 @@ class TopLevelComment extends React.Component {
|
|||
onReport,
|
||||
onRestore,
|
||||
replies,
|
||||
projectId,
|
||||
postURI,
|
||||
visibility
|
||||
} = this.props;
|
||||
|
||||
|
@ -97,7 +97,7 @@ class TopLevelComment extends React.Component {
|
|||
<FlexRow className="comment-container">
|
||||
<Comment
|
||||
highlighted={highlightedCommentId === id}
|
||||
projectId={projectId}
|
||||
postURI={postURI}
|
||||
onAddComment={this.handleAddComment}
|
||||
{...{
|
||||
author,
|
||||
|
@ -138,7 +138,7 @@ class TopLevelComment extends React.Component {
|
|||
id={reply.id}
|
||||
key={reply.id}
|
||||
parentId={id}
|
||||
projectId={projectId}
|
||||
postURI={postURI}
|
||||
replyUsername={this.authorUsername(reply.commentee_id)}
|
||||
visibility={reply.visibility}
|
||||
onAddComment={this.handleAddComment}
|
||||
|
@ -188,7 +188,7 @@ TopLevelComment.propTypes = {
|
|||
onReport: PropTypes.func,
|
||||
onRestore: PropTypes.func,
|
||||
parentId: PropTypes.number,
|
||||
projectId: PropTypes.string,
|
||||
postURI: PropTypes.string,
|
||||
replies: PropTypes.arrayOf(PropTypes.object),
|
||||
visibility: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -581,7 +581,7 @@ const PreviewPresentation = ({
|
|||
{projectInfo.comments_allowed ? (
|
||||
isLoggedIn ? (
|
||||
isShared && <ComposeComment
|
||||
projectId={projectId}
|
||||
postURI={`/proxy/comments/project/${projectId}`}
|
||||
onAddComment={onAddComment}
|
||||
/>
|
||||
) : (
|
||||
|
@ -613,7 +613,7 @@ const PreviewPresentation = ({
|
|||
key={comment.id}
|
||||
moreRepliesToLoad={comment.moreRepliesToLoad}
|
||||
parentId={comment.parent_id}
|
||||
projectId={projectId}
|
||||
postURI={`/proxy/comments/project/${projectId}`}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={onAddComment}
|
||||
|
|
|
@ -5,6 +5,7 @@ const Page = require('../../components/page/www/page.jsx');
|
|||
const render = require('../../lib/render.jsx');
|
||||
|
||||
const previewActions = require('../../redux/preview.js');
|
||||
const commentActions = require('../../redux/comments.js');
|
||||
|
||||
const isSupportedBrowser = require('../../lib/supported-browser').default;
|
||||
const UnsupportedBrowser = require('./unsupported-browser.jsx');
|
||||
|
@ -16,6 +17,7 @@ if (isSupportedBrowser()) {
|
|||
document.getElementById('app'),
|
||||
{
|
||||
preview: previewActions.previewReducer,
|
||||
comments: commentActions.commentsReducer,
|
||||
...ProjectView.guiReducers
|
||||
},
|
||||
{
|
||||
|
|
|
@ -29,6 +29,7 @@ const Meta = require('./meta.jsx');
|
|||
const sessionActions = require('../../redux/session.js');
|
||||
const navigationActions = require('../../redux/navigation.js');
|
||||
const previewActions = require('../../redux/preview.js');
|
||||
const projectCommentActions = require('../../redux/project-comment-actions.js');
|
||||
|
||||
const frameless = require('../../lib/frameless');
|
||||
|
||||
|
@ -998,7 +999,7 @@ const mapStateToProps = state => {
|
|||
canShare: userOwnsProject && state.permissions.social,
|
||||
canToggleComments: userOwnsProject || isAdmin,
|
||||
canUseBackpack: isLoggedIn,
|
||||
comments: state.preview.comments,
|
||||
comments: state.comments.comments,
|
||||
enableCommunity: projectInfoPresent,
|
||||
faved: state.preview.faved,
|
||||
favedLoaded: state.preview.status.faved === previewActions.Status.FETCHED,
|
||||
|
@ -1013,7 +1014,7 @@ const mapStateToProps = state => {
|
|||
isShared: isShared,
|
||||
loved: state.preview.loved,
|
||||
lovedLoaded: state.preview.status.loved === previewActions.Status.FETCHED,
|
||||
moreCommentsToLoad: state.preview.moreCommentsToLoad,
|
||||
moreCommentsToLoad: state.comments.moreCommentsToLoad,
|
||||
original: state.preview.original,
|
||||
parent: state.preview.parent,
|
||||
playerMode: state.scratchGui.mode.isPlayerOnly,
|
||||
|
@ -1022,7 +1023,7 @@ const mapStateToProps = state => {
|
|||
projectStudios: state.preview.projectStudios,
|
||||
registrationOpen: state.navigation.registrationOpen,
|
||||
remixes: state.preview.remixes,
|
||||
replies: state.preview.replies,
|
||||
replies: state.comments.replies,
|
||||
sessionStatus: state.session.status, // check if used
|
||||
useScratch3Registration: state.navigation.useScratch3Registration,
|
||||
user: state.session.session.user,
|
||||
|
@ -1034,16 +1035,16 @@ const mapStateToProps = state => {
|
|||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
handleAddComment: (comment, topLevelCommentId) => {
|
||||
dispatch(previewActions.addNewComment(comment, topLevelCommentId));
|
||||
dispatch(projectCommentActions.addNewComment(comment, topLevelCommentId));
|
||||
},
|
||||
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
|
||||
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
|
||||
dispatch(projectCommentActions.deleteComment(projectId, commentId, topLevelCommentId, token));
|
||||
},
|
||||
handleReportComment: (projectId, commentId, topLevelCommentId, token) => {
|
||||
dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token));
|
||||
dispatch(projectCommentActions.reportComment(projectId, commentId, topLevelCommentId, token));
|
||||
},
|
||||
handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => {
|
||||
dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token));
|
||||
dispatch(projectCommentActions.restoreComment(projectId, commentId, topLevelCommentId, token));
|
||||
},
|
||||
handleOpenRegistration: event => {
|
||||
event.preventDefault();
|
||||
|
@ -1061,8 +1062,8 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(navigationActions.toggleLoginOpen());
|
||||
},
|
||||
handleSeeAllComments: (id, ownerUsername, isAdmin, token) => {
|
||||
dispatch(previewActions.resetComments());
|
||||
dispatch(previewActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token));
|
||||
dispatch(projectCommentActions.resetComments());
|
||||
dispatch(projectCommentActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token));
|
||||
},
|
||||
handleUpdateProjectThumbnail: (id, blob) => {
|
||||
dispatch(previewActions.updateProjectThumbnail(id, blob));
|
||||
|
@ -1093,13 +1094,13 @@ const mapDispatchToProps = dispatch => ({
|
|||
}
|
||||
},
|
||||
getTopLevelComments: (id, offset, ownerUsername, isAdmin, token) => {
|
||||
dispatch(previewActions.getTopLevelComments(id, offset, ownerUsername, isAdmin, token));
|
||||
dispatch(projectCommentActions.getTopLevelComments(id, offset, ownerUsername, isAdmin, token));
|
||||
},
|
||||
getCommentById: (projectId, commentId, ownerUsername, isAdmin, token) => {
|
||||
dispatch(previewActions.getCommentById(projectId, commentId, ownerUsername, isAdmin, token));
|
||||
dispatch(projectCommentActions.getCommentById(projectId, commentId, ownerUsername, isAdmin, token));
|
||||
},
|
||||
getMoreReplies: (projectId, commentId, offset, ownerUsername, isAdmin, token) => {
|
||||
dispatch(previewActions.getReplies(projectId, [commentId], offset, ownerUsername, isAdmin, token));
|
||||
dispatch(projectCommentActions.getReplies(projectId, [commentId], offset, ownerUsername, isAdmin, token));
|
||||
},
|
||||
getFavedStatus: (id, username, token) => {
|
||||
dispatch(previewActions.getFavedStatus(id, username, token));
|
||||
|
@ -1136,7 +1137,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
remixProject: () => {
|
||||
dispatch(GUI.remixProject());
|
||||
dispatch(previewActions.resetComments());
|
||||
dispatch(projectCommentActions.resetComments());
|
||||
},
|
||||
setPlayer: player => {
|
||||
dispatch(GUI.setPlayer(player));
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"onePointFour.introNote": "{noteLabel} You can still share projects from 1.4 to the Scratch website. However, projects created in newer versions of Scratch cannot be opened in 1.4.",
|
||||
"onePointFour.downloads": "Downloads",
|
||||
"onePointFour.macTitle": "Mac OS X",
|
||||
"onePointFour.macBody": "Compatible with Mac OSX 10.4 or later",
|
||||
"onePointFour.macBody": "Compatible with Mac OSX 10.4 through 10.14",
|
||||
"onePointFour.windowsTitle": "Windows",
|
||||
"onePointFour.windowsBody": "Compatible with Windows 2000, XP, Vista, 7, and 8",
|
||||
"onePointFour.windowsNetworkInstaller": "installer",
|
||||
|
|
|
@ -1,15 +1,92 @@
|
|||
import React from 'react';
|
||||
import React, {useEffect, useCallback} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {connect} from 'react-redux';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const StudioComments = () => {
|
||||
import Button from '../../components/forms/button.jsx';
|
||||
import ComposeComment from '../preview/comment/compose-comment.jsx';
|
||||
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
|
||||
import studioCommentActions from '../../redux/studio-comment-actions.js';
|
||||
|
||||
const StudioComments = ({
|
||||
comments,
|
||||
getTopLevelComments,
|
||||
handleNewComment,
|
||||
moreCommentsToLoad,
|
||||
replies,
|
||||
shouldShowCommentComposer
|
||||
}) => {
|
||||
const {studioId} = useParams();
|
||||
|
||||
const handleLoadComments = useCallback(() => {
|
||||
getTopLevelComments(studioId, comments.length);
|
||||
}, [studioId, comments.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (comments.length === 0) getTopLevelComments(studioId, 0);
|
||||
}, [studioId]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Comments</h2>
|
||||
<p>Studio {studioId}</p>
|
||||
<div>
|
||||
{shouldShowCommentComposer &&
|
||||
<ComposeComment
|
||||
postURI={`/proxy/comments/studio/${studioId}`}
|
||||
onAddComment={handleNewComment}
|
||||
/>
|
||||
}
|
||||
{comments.map(comment => (
|
||||
<TopLevelComment
|
||||
author={comment.author}
|
||||
canReply={shouldShowCommentComposer}
|
||||
content={comment.content}
|
||||
datetimeCreated={comment.datetime_created}
|
||||
id={comment.id}
|
||||
key={comment.id}
|
||||
moreRepliesToLoad={comment.moreRepliesToLoad}
|
||||
parentId={comment.parent_id}
|
||||
postURI={`/proxy/comments/studio/${studioId}`}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={handleNewComment}
|
||||
/>
|
||||
))}
|
||||
{moreCommentsToLoad &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={handleLoadComments}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StudioComments;
|
||||
StudioComments.propTypes = {
|
||||
comments: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
getTopLevelComments: PropTypes.func,
|
||||
handleNewComment: PropTypes.func,
|
||||
moreCommentsToLoad: PropTypes.bool,
|
||||
replies: PropTypes.shape({}),
|
||||
shouldShowCommentComposer: PropTypes.bool
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
comments: state.comments.comments,
|
||||
moreCommentsToLoad: state.comments.moreCommentsToLoad,
|
||||
replies: state.comments.replies,
|
||||
|
||||
// TODO permissions like this to a selector for testing
|
||||
shouldShowCommentComposer: !!state.session.session.user // is logged in
|
||||
}),
|
||||
{
|
||||
getTopLevelComments: studioCommentActions.getTopLevelComments,
|
||||
handleNewComment: studioCommentActions.addNewComment
|
||||
}
|
||||
)(StudioComments);
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from './lib/redux-modules';
|
||||
|
||||
const {studioReducer} = require('../../redux/studio');
|
||||
const {commentsReducer} = require('../../redux/comments');
|
||||
|
||||
const StudioShell = () => {
|
||||
const match = useRouteMatch();
|
||||
|
@ -75,6 +76,7 @@ render(
|
|||
[curators.key]: curators.reducer,
|
||||
[managers.key]: managers.reducer,
|
||||
[activity.key]: activity.reducer,
|
||||
studio: studioReducer
|
||||
studio: studioReducer,
|
||||
comments: commentsReducer
|
||||
}
|
||||
);
|
||||
|
|
151
test/unit-legacy/redux/comments-test.js
Normal file
151
test/unit-legacy/redux/comments-test.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
const tap = require('tap');
|
||||
const Comments = require('../../../src/redux/comments');
|
||||
const initialState = Comments.getInitialState();
|
||||
const reducer = Comments.commentsReducer;
|
||||
|
||||
let state;
|
||||
|
||||
tap.tearDown(() => process.nextTick(process.exit));
|
||||
|
||||
tap.test('Reducer', t => {
|
||||
t.type(reducer, 'function');
|
||||
t.type(initialState, 'object');
|
||||
|
||||
// Reducers should return their default state when called without state
|
||||
let undefinedState;
|
||||
t.deepEqual(initialState, reducer(undefinedState, {type: 'fake action'}));
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setFetchStatus', t => {
|
||||
// initial value
|
||||
t.equal(initialState.status.comments, Comments.Status.NOT_FETCHED);
|
||||
|
||||
state = reducer(initialState, Comments.setFetchStatus('comments', Comments.Status.FETCHING));
|
||||
t.equal(state.status.comments, Comments.Status.FETCHING);
|
||||
|
||||
state = reducer(state, Comments.setFetchStatus('comments', Comments.Status.FETCHED));
|
||||
t.equal(state.status.comments, Comments.Status.FETCHED);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setComments', t => {
|
||||
// Initial value
|
||||
t.deepEqual(initialState.comments, []);
|
||||
|
||||
state = reducer(initialState, Comments.setComments([{id: 1}, {id: 2}]));
|
||||
state = reducer(state, Comments.setComments([{id: 3}, {id: 4}]));
|
||||
t.deepEqual(state.comments, [{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
const commentState = {
|
||||
comments: [
|
||||
{id: 'id1', visibility: 'visible'},
|
||||
{id: 'id2', visibility: 'visible'},
|
||||
{id: 'id3', visibility: 'visible'}
|
||||
],
|
||||
replies: {
|
||||
id1: [
|
||||
{id: 'id4', visibility: 'visible'},
|
||||
{id: 'id5', visibility: 'visible'}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
tap.test('setComments, discards duplicates', t => {
|
||||
state = reducer(commentState, Comments.setComments([{id: 'id1'}]));
|
||||
// Does not increase the number of comments, still 3
|
||||
t.equal(state.comments.length, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentDeleted, top level comment', t => {
|
||||
state = reducer(commentState, Comments.setCommentDeleted('id2'));
|
||||
t.equal(state.comments[1].visibility, 'deleted');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentDeleted, reply comment', t => {
|
||||
state = reducer(commentState, Comments.setCommentDeleted('id4', 'id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'deleted');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setRepliesDeleted/Restored', t => {
|
||||
state = reducer(commentState, Comments.setRepliesDeleted('id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'deleted');
|
||||
t.equal(state.replies.id1[1].visibility, 'deleted');
|
||||
|
||||
state = reducer(state, Comments.setRepliesRestored('id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'visible');
|
||||
t.equal(state.replies.id1[1].visibility, 'visible');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentReported, top level comment', t => {
|
||||
state = reducer(commentState, Comments.setCommentReported('id2'));
|
||||
t.equal(state.comments[1].visibility, 'reported');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentReported, reply comment', t => {
|
||||
state = reducer(commentState, Comments.setCommentReported('id4', 'id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'reported');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('addNewComment, top level comment', t => {
|
||||
state = reducer(commentState, Comments.addNewComment({id: 'new comment'}));
|
||||
// Adds comment to beginning of list
|
||||
t.equal(state.comments[0].id, 'new comment');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('addNewComment, reply comment', t => {
|
||||
state = reducer(commentState, Comments.addNewComment({id: 'new comment'}, 'id1'));
|
||||
// Adds replies to the end of the replies list
|
||||
t.equal(state.replies.id1[2].id, 'new comment');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setReplies', t => {
|
||||
// setReplies should append new replies
|
||||
state = reducer(commentState, Comments.setReplies({
|
||||
id1: {id: 'id6'}
|
||||
}));
|
||||
t.equal(state.replies.id1[2].id, 'id6');
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
|
||||
// setReplies should ignore duplicates, do the same as above again
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
state = reducer(state, Comments.setReplies({id1: {id: 'id6'}}));
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
|
||||
// setReplies can add replies to a comment that didn't have any
|
||||
state = reducer(state, Comments.setReplies({
|
||||
id2: {id: 'id7'}
|
||||
}));
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
t.equal(state.replies.id2.length, 1);
|
||||
t.equal(state.replies.id2[0].id, 'id7');
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[1].moreRepliesToLoad, false);
|
||||
|
||||
// Getting 20 (COMMENT_LIMIT) replies sets moreRepliesToLoad to true
|
||||
state = reducer(state, Comments.setReplies({
|
||||
id3: (new Array(20)).map((_, i) => ({id: `id${i + 1}`}))
|
||||
}));
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[1].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[2].moreRepliesToLoad, true);
|
||||
|
||||
// Getting one more reply sets moreRepliesToLoad back to false
|
||||
state = reducer(state, Comments.setReplies({
|
||||
id3: {id: 'id21'}
|
||||
}));
|
||||
t.equal(state.comments[2].moreRepliesToLoad, false);
|
||||
t.end();
|
||||
});
|
|
@ -54,123 +54,3 @@ tap.test('updateProjectInfo', t => {
|
|||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setComments', t => {
|
||||
// Initial value
|
||||
t.deepEqual(initialState.comments, []);
|
||||
|
||||
state = reducer(initialState, Preview.setComments([{id: 1}, {id: 2}]));
|
||||
state = reducer(state, Preview.setComments([{id: 3}, {id: 4}]));
|
||||
t.deepEqual(state.comments, [{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
const commentState = {
|
||||
comments: [
|
||||
{id: 'id1', visibility: 'visible'},
|
||||
{id: 'id2', visibility: 'visible'},
|
||||
{id: 'id3', visibility: 'visible'}
|
||||
],
|
||||
replies: {
|
||||
id1: [
|
||||
{id: 'id4', visibility: 'visible'},
|
||||
{id: 'id5', visibility: 'visible'}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
tap.test('setComments, discards duplicates', t => {
|
||||
state = reducer(commentState, Preview.setComments([{id: 'id1'}]));
|
||||
// Does not increase the number of comments, still 3
|
||||
t.equal(state.comments.length, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentDeleted, top level comment', t => {
|
||||
state = reducer(commentState, Preview.setCommentDeleted('id2'));
|
||||
t.equal(state.comments[1].visibility, 'deleted');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentDeleted, reply comment', t => {
|
||||
state = reducer(commentState, Preview.setCommentDeleted('id4', 'id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'deleted');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setRepliesDeleted/Restored', t => {
|
||||
state = reducer(commentState, Preview.setRepliesDeleted('id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'deleted');
|
||||
t.equal(state.replies.id1[1].visibility, 'deleted');
|
||||
|
||||
state = reducer(state, Preview.setRepliesRestored('id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'visible');
|
||||
t.equal(state.replies.id1[1].visibility, 'visible');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentReported, top level comment', t => {
|
||||
state = reducer(commentState, Preview.setCommentReported('id2'));
|
||||
t.equal(state.comments[1].visibility, 'reported');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setCommentReported, reply comment', t => {
|
||||
state = reducer(commentState, Preview.setCommentReported('id4', 'id1'));
|
||||
t.equal(state.replies.id1[0].visibility, 'reported');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('addNewComment, top level comment', t => {
|
||||
state = reducer(commentState, Preview.addNewComment({id: 'new comment'}));
|
||||
// Adds comment to beginning of list
|
||||
t.equal(state.comments[0].id, 'new comment');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('addNewComment, reply comment', t => {
|
||||
state = reducer(commentState, Preview.addNewComment({id: 'new comment'}, 'id1'));
|
||||
// Adds replies to the end of the replies list
|
||||
t.equal(state.replies.id1[2].id, 'new comment');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('setReplies', t => {
|
||||
// setReplies should append new replies
|
||||
state = reducer(commentState, Preview.setReplies({
|
||||
id1: {id: 'id6'}
|
||||
}));
|
||||
t.equal(state.replies.id1[2].id, 'id6');
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
|
||||
// setReplies should ignore duplicates, do the same as above again
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
state = reducer(state, Preview.setReplies({id1: {id: 'id6'}}));
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
|
||||
// setReplies can add replies to a comment that didn't have any
|
||||
state = reducer(state, Preview.setReplies({
|
||||
id2: {id: 'id7'}
|
||||
}));
|
||||
t.equal(state.replies.id1.length, 3);
|
||||
t.equal(state.replies.id2.length, 1);
|
||||
t.equal(state.replies.id2[0].id, 'id7');
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[1].moreRepliesToLoad, false);
|
||||
|
||||
// Getting 20 (COMMENT_LIMIT) replies sets moreRepliesToLoad to true
|
||||
state = reducer(state, Preview.setReplies({
|
||||
id3: (new Array(20)).map((_, i) => ({id: `id${i + 1}`}))
|
||||
}));
|
||||
t.equal(state.comments[0].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[1].moreRepliesToLoad, false);
|
||||
t.equal(state.comments[2].moreRepliesToLoad, true);
|
||||
|
||||
// Getting one more reply sets moreRepliesToLoad back to false
|
||||
state = reducer(state, Preview.setReplies({
|
||||
id3: {id: 'id21'}
|
||||
}));
|
||||
t.equal(state.comments[2].moreRepliesToLoad, false);
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -555,20 +555,53 @@ describe('Compose Comment test', () => {
|
|||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType set', () => {
|
||||
test('getMuteMessageInfo: muteType set and just got muted', () => {
|
||||
const justMuted = true;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
commentInstance.setState({muteType: 'unconstructive'});
|
||||
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.unconstructive');
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.unconstructive');
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted)
|
||||
.muteStepContent[0]).toBe('comment.unconstructive.content1');
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType not set', () => {
|
||||
test('getMuteMessageInfo: muteType set and already muted', () => {
|
||||
const justMuted = false;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.general');
|
||||
commentInstance.setState({muteType: 'pii'});
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.pii.past');
|
||||
// PII has the same content1 regardless of whether you were just muted
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.pii.content1');
|
||||
|
||||
commentInstance.setState({muteType: 'vulgarity'});
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.vulgarity.past');
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.type.vulgarity.past');
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType set to something we don\'t have messages for', () => {
|
||||
test('getMuteMessageInfo: muteType not set and just got muted', () => {
|
||||
const justMuted = true;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general');
|
||||
// general has the same content1 regardless of whether you were just muted
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.general.content1');
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType not set and already muted', () => {
|
||||
const justMuted = false;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general.past');
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and just got muted', () => {
|
||||
const justMuted = true;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
commentInstance.setState({muteType: 'spaghetti'});
|
||||
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.general');
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general');
|
||||
});
|
||||
|
||||
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and already muted', () => {
|
||||
const justMuted = false;
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
commentInstance.setState({muteType: 'spaghetti'});
|
||||
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general.past');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue