Merge branch 'develop' into studio-selectors

This commit is contained in:
Paul Kaplan 2021-04-02 12:36:44 -04:00
commit dc460fffde
28 changed files with 1509 additions and 1016 deletions

View file

@ -81,7 +81,7 @@ aliases:
- run: - run:
name: "setup python" name: "setup python"
command: | 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 python3 get-pip.py pip==21.0.1
pip install s3cmd==2.1.0 pip install s3cmd==2.1.0
- run: - run:
@ -134,7 +134,7 @@ jobs:
# <<: *integration_tap # <<: *integration_tap
workflows: workflows:
build-staging-production: # build-test-deploy build-test-deploy:
jobs: jobs:
- build-staging: - build-staging:
context: context:
@ -154,42 +154,39 @@ workflows:
branches: branches:
only: only:
- master - master
# - deploy-staging: - deploy-staging:
# context: context:
# - scratch-www-all - scratch-www-all
# - scratch-www-staging - scratch-www-staging
# requires: requires:
# - build-staging - build-staging
# filters: filters:
# branches: branches:
# only: only:
# - develop - develop
# - /^hotfix\/.*/ - /^hotfix\/.*/
# - /^release\/.*/ - /^release\/.*/
# - circleCI-configure-tests - integration-staging-jest:
# - integration-staging-jest: context:
# context: - scratch-www-all
# - scratch-www-all - scratch-www-staging
# - scratch-www-staging requires:
# requires: - deploy-staging
# - deploy-staging filters:
# filters: branches:
# branches: only:
# only: - develop
# - develop - /^hotfix\/.*/
# - /^hotfix\/.*/ - /^release\/.*/
# - /^release\/.*/ - integration-staging-tap:
# - circleCI-configure-tests context:
# - integration-staging-tap: - scratch-www-all
# context: - scratch-www-staging
# - scratch-www-all requires:
# - scratch-www-staging - deploy-staging
# requires: filters:
# - deploy-staging branches:
# filters: only:
# branches: - develop
# only: - /^hotfix\/.*/
# - develop - /^release\/.*/
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests

View file

@ -114,15 +114,6 @@ jobs:
include: include:
- stage: test - stage: test
deploy: deploy:
- provider: script
skip_cleanup: $SKIP_CLEANUP
script: npm run deploy
on:
repo: LLK/scratch-www
branch:
- develop
- hotfix/*
- release/*
- provider: script - provider: script
skip_cleanup: $SKIP_CLEANUP skip_cleanup: $SKIP_CLEANUP
script: npm run deploy script: npm run deploy
@ -138,6 +129,6 @@ stages:
- name: test - name: test
if: type != cron if: type != cron
- name: smoke - 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 - name: update translations
if: branch == develop AND type == cron if: branch == develop AND type == cron

842
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,9 @@
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization", "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:unit": "jest ./test/unit/ --reporters=default",
"test:unit:jest:localization": "jest ./test/localization/*.test.js --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: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", "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", "clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly", "deploy": "npm run deploy:s3 && npm run deploy:fastly",
@ -126,7 +126,7 @@
"redux-mock-store": "^1.2.3", "redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210322040541", "scratch-gui": "0.1.0-prerelease.20210401231322",
"scratch-l10n": "latest", "scratch-l10n": "latest",
"selenium-webdriver": "3.6.0", "selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",

View file

@ -4,47 +4,53 @@
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
}, },
{ {
"id": 2, "id": 2,
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
}, },
{ {
"id": 3, "id": 3,
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
}, },
{ {
"id": 4, "id": 4,
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
}, },
{ {
"id": 5, "id": 5,
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
}, },
{ {
"id": 6, "id": 6,
"type": "project", "type": "project",
"title": "Project", "title": "Project",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": "",
"href": "#" "href": "#",
"stats": {"loves": 0, "remixes": 0}
} }
] ]

View file

@ -2,129 +2,145 @@
{ {
"id": 1, "id": 1,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 2, "id": 2,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 3, "id": 3,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 4, "id": 4,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 5, "id": 5,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 6, "id": 6,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 7, "id": 7,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 8, "id": 8,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 9, "id": 9,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 10, "id": 10,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 11, "id": 11,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 12, "id": 12,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 13, "id": 13,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 14, "id": 14,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 15, "id": 15,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
}, },
{ {
"id": 16, "id": 16,
"type": "project", "type": "project",
"title": "Project", "title": "Project Title",
"thumbnailUrl": "", "thumbnailUrl": "",
"creator": "", "author": {"username": "project creator"},
"href": "#" "href": "#",
"stats": {}
} }
] ]

View file

@ -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.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.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.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.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.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.", "comments.isTooLong": "That comment is too long! Please find a way to shorten your text.",

188
src/redux/comments.js Normal file
View 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'
});

View file

@ -1,14 +1,9 @@
const defaults = require('lodash.defaults'); const defaults = require('lodash.defaults');
const keyMirror = require('keymirror'); 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 api = require('../lib/api');
const log = require('../lib/log'); const log = require('../lib/log');
const COMMENT_LIMIT = 20;
module.exports.Status = keyMirror({ module.exports.Status = keyMirror({
FETCHED: null, FETCHED: null,
NOT_FETCHED: null, NOT_FETCHED: null,
@ -19,7 +14,6 @@ module.exports.Status = keyMirror({
module.exports.getInitialState = () => ({ module.exports.getInitialState = () => ({
status: { status: {
project: module.exports.Status.NOT_FETCHED, project: module.exports.Status.NOT_FETCHED,
comments: module.exports.Status.NOT_FETCHED,
faved: module.exports.Status.NOT_FETCHED, faved: module.exports.Status.NOT_FETCHED,
loved: module.exports.Status.NOT_FETCHED, loved: module.exports.Status.NOT_FETCHED,
original: module.exports.Status.NOT_FETCHED, original: module.exports.Status.NOT_FETCHED,
@ -33,8 +27,6 @@ module.exports.getInitialState = () => ({
}, },
projectInfo: {}, projectInfo: {},
remixes: [], remixes: [],
comments: [],
replies: {},
faved: false, faved: false,
loved: false, loved: false,
original: {}, original: {},
@ -42,7 +34,6 @@ module.exports.getInitialState = () => ({
projectStudios: [], projectStudios: [],
curatedStudios: [], curatedStudios: [],
currentStudioIds: [], currentStudioIds: [],
moreCommentsToLoad: false,
projectNotAvailable: false, projectNotAvailable: false,
visibilityInfo: {} visibilityInfo: {}
}); });
@ -96,76 +87,6 @@ module.exports.previewReducer = (state, action) => {
item !== action.studioId 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': case 'SET_LOVED':
return Object.assign({}, state, { return Object.assign({}, state, {
loved: action.info loved: action.info
@ -182,10 +103,6 @@ module.exports.previewReducer = (state, action) => {
state = JSON.parse(JSON.stringify(state)); state = JSON.parse(JSON.stringify(state));
state.status.studioRequests[action.studioId] = action.status; state.status.studioRequests[action.studioId] = action.status;
return state; return state;
case 'SET_MORE_COMMENTS_TO_LOAD':
return Object.assign({}, state, {
moreCommentsToLoad: action.moreCommentsToLoad
});
case 'SET_VISIBILITY_INFO': case 'SET_VISIBILITY_INFO':
return Object.assign({}, state, { return Object.assign({}, state, {
visibilityInfo: action.visibilityInfo visibilityInfo: action.visibilityInfo
@ -247,16 +164,6 @@ module.exports.setProjectStudios = items => ({
items: 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 => ({ module.exports.setCuratedStudios = items => ({
type: 'SET_CURATED_STUDIOS', type: 'SET_CURATED_STUDIOS',
items: items items: items
@ -284,64 +191,6 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
status: 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 => ({ module.exports.setVisibilityInfo = visibilityInfo => ({
type: 'SET_VISIBILITY_INFO', type: 'SET_VISIBILITY_INFO',
visibilityInfo: visibilityInfo 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 => { module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
if (faved) { 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 => { module.exports.shareProject = (projectId, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
api({ api({

View 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
};

View 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
};

View file

@ -4,12 +4,5 @@
"pattern": "^/components/?$", "pattern": "^/components/?$",
"view": "components/components", "view": "components/components",
"title": "Components" "title": "Components"
},
{
"name": "studio",
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$",
"view": "studio/studio",
"title": "Studio Playground"
} }
] ]

View file

@ -296,6 +296,13 @@
"view": "studentregistration/studentregistration", "view": "studentregistration/studentregistration",
"title": "Class Registration" "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", "name": "teacher-faq",
"pattern": "^/educators/faq/?$", "pattern": "^/educators/faq/?$",

View file

@ -5,22 +5,81 @@ const Page = require('../../components/page/www/page.jsx');
const Box = require('../../components/box/box.jsx'); const Box = require('../../components/box/box.jsx');
const Button = require('../../components/forms/button.jsx'); const Button = require('../../components/forms/button.jsx');
const Carousel = require('../../components/carousel/carousel.jsx'); const Carousel = require('../../components/carousel/carousel.jsx');
const Form = require('../../components/forms/form.jsx');
const Input = require('../../components/forms/input.jsx'); const Input = require('../../components/forms/input.jsx');
const Spinner = require('../../components/spinner/spinner.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'); require('./components.scss');
const Components = () => ( const Components = () => (
<div className="components"> <div className="components">
<div className="inner"> <div className="inner">
<h1>Button</h1> <h1>Nav Bubbles</h1>
<Button>I love button</Button> <div className="subnavigation">
<h1>Form</h1> <SubNavigation>
<Input <a href="">
maxLength="30" <li className="active">
name="test" cats
type="text" </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> <h1>Box Component</h1>
<Box <Box
more="Cat Gifs" more="Cat Gifs"

View file

@ -5,6 +5,19 @@
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
.subnavigation {
li {
background-color: $active-gray;
&.active {
background-color: $ui-blue;
}
}
}
.form {
width: 200px;
}
.colors { .colors {
span { span {
display: inline-block; display: inline-block;

View file

@ -51,13 +51,6 @@ const ConferenceSplash = () => (
value={new Date(2021, 6, 22)} value={new Date(2021, 6, 22)}
year="numeric" year="numeric"
/> />
{' - '}
<FormattedDate
day="2-digit"
month="long"
value={new Date(2021, 6, 24)}
year="numeric"
/>
</td> </td>
</tr> </tr>
<tr className="conf2020-panel-row"> <tr className="conf2020-panel-row">

View file

@ -110,7 +110,7 @@ class Comment extends React.Component {
highlighted, highlighted,
id, id,
parentId, parentId,
projectId, postURI,
replyUsername, replyUsername,
visibility visibility
} = this.props; } = this.props;
@ -234,7 +234,7 @@ class Comment extends React.Component {
isReply isReply
commenteeId={author.id} commenteeId={author.id}
parentId={parentId || id} parentId={parentId || id}
projectId={projectId} postURI={postURI}
onAddComment={this.handlePostReply} onAddComment={this.handlePostReply}
onCancel={this.handleToggleReplying} onCancel={this.handleToggleReplying}
/> />
@ -285,7 +285,7 @@ Comment.propTypes = {
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func, onRestore: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
replyUsername: PropTypes.string, replyUsername: PropTypes.string,
visibility: PropTypes.string visibility: PropTypes.string
}; };

View file

@ -79,7 +79,7 @@ class ComposeComment extends React.Component {
handlePost () { handlePost () {
this.setState({status: ComposeStatus.SUBMITTING}); this.setState({status: ComposeStatus.SUBMITTING});
api({ api({
uri: `/proxy/comments/project/${this.props.projectId}`, uri: this.props.postURI,
authentication: this.props.user.token, authentication: this.props.user.token,
withCredentials: true, withCredentials: true,
method: 'POST', method: 'POST',
@ -224,42 +224,43 @@ class ComposeComment extends React.Component {
MuteModal.steps.MUTE_INFO : MuteModal.steps.COMMENT_ISSUE; MuteModal.steps.MUTE_INFO : MuteModal.steps.COMMENT_ISSUE;
} }
getMuteMessageInfo () { getMuteMessageInfo (justMuted) {
// return the ids for the messages that are shown for this mute type // 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 // If mute modals have more than one unique "step" we could pass an array of steps
const messageInfo = { const messageInfo = {
pii: { pii: {
name: 'pii', name: 'pii',
commentType: 'comment.type.pii', commentType: justMuted ? 'comment.type.pii' : 'comment.type.pii.past',
commentTypePast: 'comment.type.pii.past',
muteStepHeader: 'comment.pii.header', muteStepHeader: 'comment.pii.header',
muteStepContent: ['comment.pii.content1', 'comment.pii.content2', 'comment.pii.content3'] muteStepContent: ['comment.pii.content1', 'comment.pii.content2', 'comment.pii.content3']
}, },
unconstructive: { unconstructive: {
name: 'unconstructive', name: 'unconstructive',
commentType: 'comment.type.unconstructive', commentType: justMuted ? 'comment.type.unconstructive' : 'comment.type.unconstructive.past',
commentTypePast: 'comment.type.unconstructive.past',
muteStepHeader: 'comment.unconstructive.header', muteStepHeader: 'comment.unconstructive.header',
muteStepContent: ['comment.unconstructive.content1', 'comment.unconstructive.content2'] muteStepContent: [
justMuted ? 'comment.unconstructive.content1' : 'comment.type.unconstructive.past',
'comment.unconstructive.content2'
]
}, },
vulgarity: { vulgarity: {
name: 'vulgarity', name: 'vulgarity',
commentType: 'comment.type.vulgarity', commentType: justMuted ? 'comment.type.vulgarity' : 'comment.type.vulgarity.past',
commentTypePast: 'comment.type.vulgarity.past',
muteStepHeader: 'comment.vulgarity.header', muteStepHeader: 'comment.vulgarity.header',
muteStepContent: ['comment.vulgarity.content1', 'comment.vulgarity.content2'] muteStepContent: [
justMuted ? 'comment.vulgarity.content1' : 'comment.type.vulgarity.past',
'comment.vulgarity.content2'
]
}, },
spam: { spam: {
name: 'spam', name: 'spam',
commentType: 'comment.type.spam', commentType: justMuted ? 'comment.type.spam' : 'comment.type.spam.past',
commentTypePast: 'comment.type.spam.past',
muteStepHeader: 'comment.spam.header', muteStepHeader: 'comment.spam.header',
muteStepContent: ['comment.spam.content1', 'comment.spam.content2'] muteStepContent: ['comment.spam.content1', 'comment.spam.content2']
}, },
general: { general: {
name: 'general', name: 'general',
commentType: 'comment.type.general', commentType: justMuted ? 'comment.type.general' : 'comment.type.general.past',
commentTypePast: 'comment.type.general.past',
muteStepHeader: 'comment.general.header', muteStepHeader: 'comment.general.header',
muteStepContent: ['comment.general.content1'] muteStepContent: ['comment.general.content1']
} }
@ -292,9 +293,9 @@ class ComposeComment extends React.Component {
<p> <p>
<FormattedMessage <FormattedMessage
id={ id={
this.state.status === ComposeStatus.REJECTED_MUTE ? this.getMuteMessageInfo(
this.getMuteMessageInfo().commentType : this.state.status === ComposeStatus.REJECTED_MUTE
this.getMuteMessageInfo().commentTypePast ).commentType
} }
/> />
</p> </p>
@ -404,7 +405,7 @@ class ComposeComment extends React.Component {
useStandardSizes useStandardSizes
className="mod-mute" className="mod-mute"
commentContent={this.state.message} commentContent={this.state.message}
muteModalMessages={this.getMuteMessageInfo()} muteModalMessages={this.getMuteMessageInfo(this.state.status === ComposeStatus.REJECTED_MUTE)}
shouldCloseOnOverlayClick={false} shouldCloseOnOverlayClick={false}
showFeedback={ showFeedback={
this.state.status === ComposeStatus.REJECTED_MUTE this.state.status === ComposeStatus.REJECTED_MUTE
@ -433,7 +434,7 @@ ComposeComment.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
username: PropTypes.string, username: PropTypes.string,

View file

@ -87,7 +87,7 @@ class TopLevelComment extends React.Component {
onReport, onReport,
onRestore, onRestore,
replies, replies,
projectId, postURI,
visibility visibility
} = this.props; } = this.props;
@ -97,7 +97,7 @@ class TopLevelComment extends React.Component {
<FlexRow className="comment-container"> <FlexRow className="comment-container">
<Comment <Comment
highlighted={highlightedCommentId === id} highlighted={highlightedCommentId === id}
projectId={projectId} postURI={postURI}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
{...{ {...{
author, author,
@ -138,7 +138,7 @@ class TopLevelComment extends React.Component {
id={reply.id} id={reply.id}
key={reply.id} key={reply.id}
parentId={id} parentId={id}
projectId={projectId} postURI={postURI}
replyUsername={this.authorUsername(reply.commentee_id)} replyUsername={this.authorUsername(reply.commentee_id)}
visibility={reply.visibility} visibility={reply.visibility}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
@ -188,7 +188,7 @@ TopLevelComment.propTypes = {
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func, onRestore: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, postURI: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.arrayOf(PropTypes.object),
visibility: PropTypes.string visibility: PropTypes.string
}; };

View file

@ -581,7 +581,7 @@ const PreviewPresentation = ({
{projectInfo.comments_allowed ? ( {projectInfo.comments_allowed ? (
isLoggedIn ? ( isLoggedIn ? (
isShared && <ComposeComment isShared && <ComposeComment
projectId={projectId} postURI={`/proxy/comments/project/${projectId}`}
onAddComment={onAddComment} onAddComment={onAddComment}
/> />
) : ( ) : (
@ -613,7 +613,7 @@ const PreviewPresentation = ({
key={comment.id} key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad} moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id} parentId={comment.parent_id}
projectId={projectId} postURI={`/proxy/comments/project/${projectId}`}
replies={replies && replies[comment.id] ? replies[comment.id] : []} replies={replies && replies[comment.id] ? replies[comment.id] : []}
visibility={comment.visibility} visibility={comment.visibility}
onAddComment={onAddComment} onAddComment={onAddComment}

View file

@ -5,6 +5,7 @@ const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx'); const render = require('../../lib/render.jsx');
const previewActions = require('../../redux/preview.js'); const previewActions = require('../../redux/preview.js');
const commentActions = require('../../redux/comments.js');
const isSupportedBrowser = require('../../lib/supported-browser').default; const isSupportedBrowser = require('../../lib/supported-browser').default;
const UnsupportedBrowser = require('./unsupported-browser.jsx'); const UnsupportedBrowser = require('./unsupported-browser.jsx');
@ -16,6 +17,7 @@ if (isSupportedBrowser()) {
document.getElementById('app'), document.getElementById('app'),
{ {
preview: previewActions.previewReducer, preview: previewActions.previewReducer,
comments: commentActions.commentsReducer,
...ProjectView.guiReducers ...ProjectView.guiReducers
}, },
{ {

View file

@ -29,6 +29,7 @@ const Meta = require('./meta.jsx');
const sessionActions = require('../../redux/session.js'); const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js'); const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js'); const previewActions = require('../../redux/preview.js');
const projectCommentActions = require('../../redux/project-comment-actions.js');
const frameless = require('../../lib/frameless'); const frameless = require('../../lib/frameless');
@ -998,7 +999,7 @@ const mapStateToProps = state => {
canShare: userOwnsProject && state.permissions.social, canShare: userOwnsProject && state.permissions.social,
canToggleComments: userOwnsProject || isAdmin, canToggleComments: userOwnsProject || isAdmin,
canUseBackpack: isLoggedIn, canUseBackpack: isLoggedIn,
comments: state.preview.comments, comments: state.comments.comments,
enableCommunity: projectInfoPresent, enableCommunity: projectInfoPresent,
faved: state.preview.faved, faved: state.preview.faved,
favedLoaded: state.preview.status.faved === previewActions.Status.FETCHED, favedLoaded: state.preview.status.faved === previewActions.Status.FETCHED,
@ -1013,7 +1014,7 @@ const mapStateToProps = state => {
isShared: isShared, isShared: isShared,
loved: state.preview.loved, loved: state.preview.loved,
lovedLoaded: state.preview.status.loved === previewActions.Status.FETCHED, lovedLoaded: state.preview.status.loved === previewActions.Status.FETCHED,
moreCommentsToLoad: state.preview.moreCommentsToLoad, moreCommentsToLoad: state.comments.moreCommentsToLoad,
original: state.preview.original, original: state.preview.original,
parent: state.preview.parent, parent: state.preview.parent,
playerMode: state.scratchGui.mode.isPlayerOnly, playerMode: state.scratchGui.mode.isPlayerOnly,
@ -1022,7 +1023,7 @@ const mapStateToProps = state => {
projectStudios: state.preview.projectStudios, projectStudios: state.preview.projectStudios,
registrationOpen: state.navigation.registrationOpen, registrationOpen: state.navigation.registrationOpen,
remixes: state.preview.remixes, remixes: state.preview.remixes,
replies: state.preview.replies, replies: state.comments.replies,
sessionStatus: state.session.status, // check if used sessionStatus: state.session.status, // check if used
useScratch3Registration: state.navigation.useScratch3Registration, useScratch3Registration: state.navigation.useScratch3Registration,
user: state.session.session.user, user: state.session.session.user,
@ -1034,16 +1035,16 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
handleAddComment: (comment, topLevelCommentId) => { handleAddComment: (comment, topLevelCommentId) => {
dispatch(previewActions.addNewComment(comment, topLevelCommentId)); dispatch(projectCommentActions.addNewComment(comment, topLevelCommentId));
}, },
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => { handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token)); dispatch(projectCommentActions.deleteComment(projectId, commentId, topLevelCommentId, token));
}, },
handleReportComment: (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) => { handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token)); dispatch(projectCommentActions.restoreComment(projectId, commentId, topLevelCommentId, token));
}, },
handleOpenRegistration: event => { handleOpenRegistration: event => {
event.preventDefault(); event.preventDefault();
@ -1061,8 +1062,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(navigationActions.toggleLoginOpen()); dispatch(navigationActions.toggleLoginOpen());
}, },
handleSeeAllComments: (id, ownerUsername, isAdmin, token) => { handleSeeAllComments: (id, ownerUsername, isAdmin, token) => {
dispatch(previewActions.resetComments()); dispatch(projectCommentActions.resetComments());
dispatch(previewActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token)); dispatch(projectCommentActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token));
}, },
handleUpdateProjectThumbnail: (id, blob) => { handleUpdateProjectThumbnail: (id, blob) => {
dispatch(previewActions.updateProjectThumbnail(id, blob)); dispatch(previewActions.updateProjectThumbnail(id, blob));
@ -1093,13 +1094,13 @@ const mapDispatchToProps = dispatch => ({
} }
}, },
getTopLevelComments: (id, offset, ownerUsername, isAdmin, token) => { 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) => { 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) => { 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) => { getFavedStatus: (id, username, token) => {
dispatch(previewActions.getFavedStatus(id, username, token)); dispatch(previewActions.getFavedStatus(id, username, token));
@ -1136,7 +1137,7 @@ const mapDispatchToProps = dispatch => ({
}, },
remixProject: () => { remixProject: () => {
dispatch(GUI.remixProject()); dispatch(GUI.remixProject());
dispatch(previewActions.resetComments()); dispatch(projectCommentActions.resetComments());
}, },
setPlayer: player => { setPlayer: player => {
dispatch(GUI.setPlayer(player)); dispatch(GUI.setPlayer(player));

View file

@ -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.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.downloads": "Downloads",
"onePointFour.macTitle": "Mac OS X", "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.windowsTitle": "Windows",
"onePointFour.windowsBody": "Compatible with Windows 2000, XP, Vista, 7, and 8", "onePointFour.windowsBody": "Compatible with Windows 2000, XP, Vista, 7, and 8",
"onePointFour.windowsNetworkInstaller": "installer", "onePointFour.windowsNetworkInstaller": "installer",

View file

@ -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 {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 {studioId} = useParams();
const handleLoadComments = useCallback(() => {
getTopLevelComments(studioId, comments.length);
}, [studioId, comments.length]);
useEffect(() => {
if (comments.length === 0) getTopLevelComments(studioId, 0);
}, [studioId]);
return ( return (
<div> <div>
<h2>Comments</h2> <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> </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);

View file

@ -25,6 +25,7 @@ import {
} from './lib/redux-modules'; } from './lib/redux-modules';
const {studioReducer} = require('../../redux/studio'); const {studioReducer} = require('../../redux/studio');
const {commentsReducer} = require('../../redux/comments');
const StudioShell = () => { const StudioShell = () => {
const match = useRouteMatch(); const match = useRouteMatch();
@ -75,6 +76,7 @@ render(
[curators.key]: curators.reducer, [curators.key]: curators.reducer,
[managers.key]: managers.reducer, [managers.key]: managers.reducer,
[activity.key]: activity.reducer, [activity.key]: activity.reducer,
studio: studioReducer studio: studioReducer,
comments: commentsReducer
} }
); );

View 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();
});

View file

@ -54,123 +54,3 @@ tap.test('updateProjectInfo', t => {
}); });
t.end(); 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();
});

View file

@ -555,20 +555,53 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow; global.Date.now = realDateNow;
}); });
test('getMuteMessageInfo: muteType set', () => { test('getMuteMessageInfo: muteType set and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance(); const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'unconstructive'}); 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(); 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(); const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'spaghetti'}); 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');
}); });
}); });