Muted users can’t edit studios

linting fixes
This commit is contained in:
seotts 2021-05-14 15:20:29 -04:00
parent b7afdae8cd
commit 19a9997e0f
10 changed files with 128 additions and 36 deletions

View file

@ -8,6 +8,7 @@
background-color: $ui-blue-10percent;
width: 100%;
text-align: center;
box-sizing: border-box;
p {
margin-bottom: 0;

View file

@ -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);

View file

@ -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

View file

@ -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
});

View file

@ -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."
}

View file

@ -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

View file

@ -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);

View file

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

View file

@ -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
}
}
}
}
}

View file

@ -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);