diff --git a/src/components/commenting-status/commenting-status.scss b/src/components/commenting-status/commenting-status.scss index 4b6e85c11..8e0e50e5a 100644 --- a/src/components/commenting-status/commenting-status.scss +++ b/src/components/commenting-status/commenting-status.scss @@ -8,6 +8,7 @@ background-color: $ui-blue-10percent; width: 100%; text-align: center; + box-sizing: border-box; p { margin-bottom: 0; diff --git a/src/redux/session.js b/src/redux/session.js index b6ed97cc4..4b0063f6f 100644 --- a/src/redux/session.js +++ b/src/redux/session.js @@ -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(); + module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED; // NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN diff --git a/src/redux/studio-permissions.js b/src/redux/studio-permissions.js index 25c445fe0..0afdb2735 100644 --- a/src/redux/studio-permissions.js +++ b/src/redux/studio-permissions.js @@ -1,4 +1,5 @@ -const {selectUserId, selectIsAdmin, selectIsSocial, selectIsLoggedIn, selectUsername} = require('./session'); +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 +7,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 +28,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 +44,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 @@ -58,6 +63,15 @@ const selectCanRemoveProject = (state, creatorUsername, actorId) => { return false; }; +// We should only show the mute errors to muted users who have any permissions related to the content +const selectShowEditMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state)); +const selectShowProjectMuteError = state => selectIsMuted(state) && + (selectIsAdmin(state) || + isManager(state) || + isCurator(state) || + (selectIsSocial(state) && state.studio.openToAll)); +const selectShowCuratorMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state)); + export { selectCanEditInfo, selectCanAddProjects, @@ -74,5 +88,8 @@ export { selectCanRemoveCurator, selectCanRemoveManager, selectCanPromoteCurators, - selectCanRemoveProject + selectCanRemoveProject, + selectShowEditMuteError, + selectShowProjectMuteError, + selectShowCuratorMuteError }; diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx index 4f2fc532e..ebe19e789 100644 --- a/src/views/preview/comment/compose-comment.jsx +++ b/src/views/preview/comment/compose-comment.jsx @@ -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 }); diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json index 691ebfc26..443a702d5 100644 --- a/src/views/studio/l10n.json +++ b/src/views/studio/l10n.json @@ -84,6 +84,11 @@ "studio.reportThanksForLettingUsKnow": "Thanks for letting us know!", "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.mutedEdit": "You will be able to edit studios again {inDuration}.", + "studios.mutedPaused": "Your account has been paused from using studios until then.", + "studio.alertProjectAdded": "\"{title}\" added to studio", "studio.alertProjectAlreadyAdded": "That project is already in this studio", "studio.alertProjectRemoveError": "Something went wrong removing the project", diff --git a/src/views/studio/studio-description.jsx b/src/views/studio/studio-description.jsx index 1fb0e5e39..11a16f7ff 100644 --- a/src/views/studio/studio-description.jsx +++ b/src/views/studio/studio-description.jsx @@ -1,18 +1,19 @@ /* eslint-disable react/jsx-no-bind */ -import React from 'react'; +import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio'; -import {selectCanEditInfo} from '../../redux/studio-permissions'; +import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions'; import { Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError } from '../../redux/studio-mutations'; import ValidationMessage from '../../components/forms/validation-message.jsx'; import decorateText from '../../lib/decorate-text.jsx'; +import StudioMuteEditMessage from './studio-mute-edit-message.jsx'; const errorToMessageId = error => { switch (error) { @@ -24,21 +25,29 @@ const errorToMessageId = error => { }; const StudioDescription = ({ - descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate + descriptionError, isFetching, isMutating, isMutedEditor, description, canEditInfo, handleUpdate }) => { + const [showMuteMessage, setShowMuteMessage] = useState(false); + const fieldClassName = classNames('studio-description', { 'mod-fetching': isFetching, 'mod-mutating': isMutating, - 'mod-form-error': !!descriptionError + 'mod-form-error': !!descriptionError, + 'muted-editor': showMuteMessage }); + return ( -
- {canEditInfo ? ( +
isMutedEditor && setShowMuteMessage(true)} + onMouseLeave={() => isMutedEditor && setShowMuteMessage(false)} + > + {canEditInfo || isMutedEditor ? (