mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-03-25 12:20:24 -04:00
Muted users can’t edit studios
linting fixes
This commit is contained in:
parent
b7afdae8cd
commit
19a9997e0f
10 changed files with 128 additions and 36 deletions
src
components/commenting-status
redux
views
test
|
@ -8,6 +8,7 @@
|
|||
background-color: $ui-blue-10percent;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -128,6 +128,10 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user',
|
|||
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
|
||||
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
|
||||
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
|
||||
module.exports.selectMuteStatus = state => get(state, ['session', 'session', 'permissions', 'mute_status'],
|
||||
{muteExpiresAt: 0, offenses: [], showWarning: false});
|
||||
module.exports.selectIsMuted = state => (module.exports.selectMuteStatus(state).muteExpiresAt || 0) * 1000 > Date.now();
|
||||
|
||||
|
||||
// NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN
|
||||
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const {selectUserId, selectIsAdmin, selectIsSocial, selectIsLoggedIn, selectUsername} = require('./session');
|
||||
import { false } from 'tap';
|
||||
|
||||
const {selectUserId, selectIsAdmin, selectIsSocial,
|
||||
selectIsLoggedIn, selectUsername, selectIsMuted} = require('./session');
|
||||
|
||||
// Fine-grain selector helpers - not exported, use the higher level selectors below
|
||||
const isCreator = state => selectUserId(state) === state.studio.owner;
|
||||
|
@ -6,11 +9,12 @@ const isCurator = state => state.studio.curator;
|
|||
const isManager = state => state.studio.manager || isCreator(state);
|
||||
|
||||
// Action-based permissions selectors
|
||||
const selectCanEditInfo = state => selectIsAdmin(state) || isManager(state);
|
||||
const selectCanEditInfo = state => !selectIsMuted(state) && (selectIsAdmin(state) || isManager(state));
|
||||
const selectCanAddProjects = state =>
|
||||
isManager(state) ||
|
||||
!selectIsMuted(state) &&
|
||||
(isManager(state) ||
|
||||
isCurator(state) ||
|
||||
(selectIsSocial(state) && state.studio.openToAll);
|
||||
(selectIsSocial(state) && state.studio.openToAll));
|
||||
|
||||
// This isn't "canComment" since they could be muted, but comment composer handles that
|
||||
const selectShowCommentComposer = state => selectIsSocial(state);
|
||||
|
@ -26,12 +30,13 @@ const selectCanDeleteCommentWithoutConfirm = state => selectIsAdmin(state);
|
|||
const selectCanFollowStudio = state => selectIsLoggedIn(state);
|
||||
|
||||
// Matching existing behavior, only admin/creator is allowed to toggle comments.
|
||||
const selectCanEditCommentsAllowed = state => selectIsAdmin(state) || isCreator(state);
|
||||
const selectCanEditOpenToAll = state => isManager(state);
|
||||
const selectCanEditCommentsAllowed = state => !selectIsMuted(state) && (selectIsAdmin(state) || isCreator(state));
|
||||
const selectCanEditOpenToAll = state => !selectIsMuted(state) && isManager(state);
|
||||
|
||||
const selectShowCuratorInvite = state => !!state.studio.invited;
|
||||
const selectCanInviteCurators = state => isManager(state);
|
||||
const selectShowCuratorInvite = state => !selectIsMuted(state) && !!state.studio.invited;
|
||||
const selectCanInviteCurators = state => !selectIsMuted(state) && isManager(state);
|
||||
const selectCanRemoveCurator = (state, username) => {
|
||||
if (selectIsMuted(state)) return false;
|
||||
// Admins/managers can remove any curators
|
||||
if (isManager(state) || selectIsAdmin(state)) return true;
|
||||
// Curators can remove themselves
|
||||
|
@ -41,10 +46,12 @@ const selectCanRemoveCurator = (state, username) => {
|
|||
return false;
|
||||
};
|
||||
const selectCanRemoveManager = (state, managerId) =>
|
||||
(selectIsAdmin(state) || isManager(state)) && managerId !== state.studio.owner;
|
||||
const selectCanPromoteCurators = state => isManager(state);
|
||||
!selectIsMuted(state) && (selectIsAdmin(state) || isManager(state)) && managerId !== state.studio.owner;
|
||||
const selectCanPromoteCurators = state => !selectIsMuted(state) && isManager(state);
|
||||
|
||||
const selectCanRemoveProject = (state, creatorUsername, actorId) => {
|
||||
if (selectIsMuted(state)) return false;
|
||||
|
||||
// Admins/managers can remove any projects
|
||||
if (isManager(state) || selectIsAdmin(state)) return true;
|
||||
// Project owners can always remove their projects
|
||||
|
|
|
@ -17,6 +17,7 @@ const formatTime = require('../../../lib/format-time');
|
|||
const connect = require('react-redux').connect;
|
||||
|
||||
const api = require('../../../lib/api');
|
||||
const {selectMuteStatus} = require('../../../redux/session.js');
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
|
@ -444,9 +445,7 @@ ComposeComment.propTypes = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
muteStatus: state.session.session.permissions.mute_status ?
|
||||
state.session.session.permissions.mute_status :
|
||||
{muteExpiresAt: 0, offenses: [], showWarning: false},
|
||||
muteStatus: selectMuteStatus(state),
|
||||
user: state.session.session.user
|
||||
});
|
||||
|
||||
|
|
|
@ -73,5 +73,9 @@
|
|||
"studio.reportPleaseExplain": "Please select which part of the studio you find to be disrespectful or inappropriate, or otherwise breaks the Scratch Community Guidelines.",
|
||||
"studio.reportAreThereComments": "Are there inappropriate comments in the studio? Please report them by clicking the \"report\" button on the individual comments.",
|
||||
"studio.reportThanksForLettingUsKnow": "Thanks for letting us know!",
|
||||
"studio.reportYourFeedback": "Your feedback will help us make Scratch better."
|
||||
"studio.reportYourFeedback": "Your feedback will help us make Scratch better.",
|
||||
|
||||
"studios.mutedCurators": "You will be able to invite curators and add managers again {inDuration}.",
|
||||
"studios.mutedProjects": "You will be able to add and remove projects again {inDuration}.",
|
||||
"studios.mutedPaused": "Your account has been paused from using studios until then."
|
||||
}
|
||||
|
|
|
@ -11,9 +11,12 @@ import StudioProjectAdder from './studio-project-adder.jsx';
|
|||
import StudioProjectTile from './studio-project-tile.jsx';
|
||||
import {loadProjects} from './lib/studio-project-actions.js';
|
||||
import classNames from 'classnames';
|
||||
import CommentingStatus from '../../components/commenting-status/commenting-status.jsx';
|
||||
import {selectMuteStatus} from '../../redux/session.js';
|
||||
import {formatRelativeTime} from '../../lib/format-time.js';
|
||||
|
||||
const StudioProjects = ({
|
||||
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
|
||||
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore, muteExpiresAtMs
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (items.length === 0) onLoadMore();
|
||||
|
@ -25,6 +28,19 @@ const StudioProjects = ({
|
|||
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
</div>
|
||||
{muteExpiresAtMs > Date.now() &&
|
||||
<CommentingStatus>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="studios.mutedCurators"
|
||||
values={{
|
||||
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
|
||||
}}
|
||||
/>
|
||||
<FormattedMessage id="studios.mutedPaused" />
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
}
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
|
@ -108,6 +124,7 @@ StudioProjects.propTypes = {
|
|||
loading: PropTypes.bool,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
moreToLoad: PropTypes.bool,
|
||||
muteExpiresAtMs: PropTypes.number,
|
||||
onLoadMore: PropTypes.func
|
||||
};
|
||||
|
||||
|
@ -115,7 +132,8 @@ export default connect(
|
|||
state => ({
|
||||
...projects.selector(state),
|
||||
canAddProjects: selectCanAddProjects(state),
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state)
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state),
|
||||
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)
|
||||
}),
|
||||
{
|
||||
onLoadMore: loadProjects
|
||||
|
|
|
@ -38,8 +38,12 @@ const {commentsReducer} = require('../../redux/comments');
|
|||
const {studioMutationsReducer} = require('../../redux/studio-mutations');
|
||||
|
||||
import './studio.scss';
|
||||
import {selectIsMuted, selectMuteStatus} from '../../redux/session.js';
|
||||
import {formatRelativeTime} from '../../lib/format-time.js';
|
||||
import CommentingStatus from '../../components/commenting-status/commenting-status.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const StudioShell = ({studioLoadFailed}) => {
|
||||
const StudioShell = ({isMuted, muteExpiresAtMs, studioLoadFailed}) => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
|
@ -54,6 +58,19 @@ const StudioShell = ({studioLoadFailed}) => {
|
|||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
{isMuted &&
|
||||
<CommentingStatus className="studio-curator-mute-box">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="studios.mutedCurators"
|
||||
values={{
|
||||
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
|
||||
}}
|
||||
/>
|
||||
<FormattedMessage id="studios.mutedPaused" />
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
}
|
||||
<StudioManagers />
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
|
@ -78,12 +95,16 @@ const StudioShell = ({studioLoadFailed}) => {
|
|||
};
|
||||
|
||||
StudioShell.propTypes = {
|
||||
isMuted: PropTypes.bool,
|
||||
muteExpiresAtMs: PropTypes.number,
|
||||
studioLoadFailed: PropTypes.bool
|
||||
};
|
||||
|
||||
const ConnectedStudioShell = connect(
|
||||
state => ({
|
||||
studioLoadFailed: selectStudioLoadFailed(state)
|
||||
isMuted: selectIsMuted(state),
|
||||
studioLoadFailed: selectStudioLoadFailed(state),
|
||||
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)
|
||||
}),
|
||||
)(StudioShell);
|
||||
|
||||
|
|
|
@ -427,4 +427,8 @@ $radius: 8px;
|
|||
|
||||
.mod-form-error { /* When a field contains a value is causing an error */
|
||||
border-color: $ui-orange !important;
|
||||
}
|
||||
|
||||
.studio-curator-mute-box {
|
||||
margin: 20px 0;
|
||||
}
|
|
@ -50,6 +50,19 @@
|
|||
"social": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"isMuted": {
|
||||
"session": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "user1-username",
|
||||
"token": "user1-token"
|
||||
},
|
||||
"permissions": {
|
||||
"mute_status": {"muteExpiresAt": 32515480478, "offenses": [], "showWarning": false},
|
||||
"social": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,9 @@ const setStateByRole = (role) => {
|
|||
case 'invited':
|
||||
state.studio = studios.isInvited;
|
||||
break;
|
||||
case 'muted':
|
||||
state.session = sessions.isMuted;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown user role in test: ' + role);
|
||||
}
|
||||
|
@ -72,7 +75,8 @@ describe('studio info', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditInfo(state)).toBe(expected);
|
||||
|
@ -89,7 +93,8 @@ describe('studio projects', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanAddProjects(state)).toBe(expected);
|
||||
|
@ -100,7 +105,8 @@ describe('studio projects', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.openToAll = true;
|
||||
|
@ -116,7 +122,8 @@ describe('studio projects', () => {
|
|||
['creator', true],
|
||||
['logged in', false], // false for projects that are not theirs, see below
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveProject(state, 'not-me', 'not-me')).toBe(expected);
|
||||
|
@ -147,7 +154,8 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', true] // comment composer is there, but contains muted ComposeStatus
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCommentComposer(state)).toBe(expected);
|
||||
|
@ -158,7 +166,8 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanReportComment(state)).toBe(expected);
|
||||
|
@ -173,7 +182,8 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanDeleteComment(state)).toBe(expected);
|
||||
|
@ -188,7 +198,8 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanDeleteCommentWithoutConfirm(state)).toBe(expected);
|
||||
|
@ -203,7 +214,8 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRestoreComment(state)).toBe(expected);
|
||||
|
@ -214,7 +226,8 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', true],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanFollowStudio(state)).toBe(expected);
|
||||
|
@ -229,7 +242,8 @@ describe('studio comments', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditCommentsAllowed(state)).toBe(expected);
|
||||
|
@ -244,7 +258,8 @@ describe('studio comments', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditOpenToAll(state)).toBe(expected);
|
||||
|
@ -262,7 +277,8 @@ describe('studio members', () => {
|
|||
['invited', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCuratorInvite(state)).toBe(expected);
|
||||
|
@ -277,7 +293,8 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanPromoteCurators(state)).toBe(expected);
|
||||
|
@ -292,7 +309,8 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveCurator(state, 'others-username')).toBe(expected);
|
||||
|
@ -313,7 +331,8 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveManager(state, '123')).toBe(expected);
|
||||
|
@ -327,7 +346,8 @@ describe('studio members', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.owner = 'the creator';
|
||||
|
@ -344,7 +364,8 @@ describe('studio members', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanInviteCurators(state)).toBe(expected);
|
||||
|
|
Loading…
Add table
Reference in a new issue