Merge branch 'develop' into studio-comments-off

This commit is contained in:
picklesrus 2021-06-17 13:47:03 -04:00 committed by GitHub
commit d3e368bce2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 483 additions and 391 deletions

43
package-lock.json generated
View file

@ -61,20 +61,20 @@
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"optional": true,
"requires": {
"anymatch": "~3.1.1",
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
"readdirp": "~3.6.0"
}
},
"commander": {
@ -176,9 +176,9 @@
"optional": true
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"optional": true,
"requires": {
@ -20873,9 +20873,9 @@
}
},
"scratch-blocks": {
"version": "0.1.0-prerelease.20210615035054",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210615035054.tgz",
"integrity": "sha512-/YGZN3QiMa41gtcyHUUUVTfhztoT7zEb0+N+FeBmtbkZfeESOtEugJ0y0ftttUY7WJRga35TTDp1UdEyo7sxAg==",
"version": "0.1.0-prerelease.20210616144308",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210616144308.tgz",
"integrity": "sha512-ahYux5Xn0EfkQpBtcwJ641Idu/oQutrpVTqLGf92cVyRqJp3LR/end6HshCCc4l6YFJBGOZV9bKjnsRkktnJRw==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@ -20883,9 +20883,9 @@
}
},
"scratch-gui": {
"version": "0.1.0-prerelease.20210615041617",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210615041617.tgz",
"integrity": "sha512-HVmqbo9MLfV5tvc5owt7BT+fJKj5QRrz+c4S2vc8WyAsxMEerAYKfaNCHI89LmA5CRb42sgb0Z6YnwlsNVsaiw==",
"version": "0.1.0-prerelease.20210617035831",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210617035831.tgz",
"integrity": "sha512-JldjubVfCWtT69H8/0eidMNUOnUxNAtuP62p92yhmwXsOHlgh/BSbns9BUaWZaryDYWuEOM0NodSWuiw+xO/9w==",
"dev": true,
"requires": {
"arraybuffer-loader": "^1.0.6",
@ -20936,8 +20936,8 @@
"redux": "3.7.2",
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20210615035054",
"scratch-l10n": "3.12.20210615031544",
"scratch-blocks": "0.1.0-prerelease.20210616144308",
"scratch-l10n": "3.13.20210617031540",
"scratch-paint": "0.2.0-prerelease.20210615011117",
"scratch-render": "0.1.0-prerelease.20210325231800",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
@ -21394,15 +21394,14 @@
}
},
"scratch-l10n": {
"version": "3.12.20210615031544",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.12.20210615031544.tgz",
"integrity": "sha512-8U2y0wu+xy29ayND5bY4odklo9D/5mVW1XQ+YrBx7rykUPHiJOzPUYHvTUQVXC0CI8khAtW98Lt3SI9m4MxBuw==",
"version": "3.13.20210617031540",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.13.20210617031540.tgz",
"integrity": "sha512-DPHqWiRnHv7Rp7s3EeESPpFgnHODboZyeRmR133vRj0j7DBnfwtzodzm659J8j4vyudcQblGbtcJYeRHbnYvpA==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"babel-plugin-react-intl": "^3.0.1",
"react-intl": "^2.8.0",
"transifex": "1.6.6"
}
},

View file

@ -126,8 +126,8 @@
"redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210615041617",
"scratch-l10n": "3.12.20210615031544",
"scratch-gui": "0.1.0-prerelease.20210617035831",
"scratch-l10n": "3.13.20210617031540",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",

View file

@ -147,8 +147,6 @@ module.exports.selectStudioCommentsGloballyEnabled = state =>
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.selectNewStudiosLaunched = state => get(state, ['session', 'session', 'flags', 'new_studios_launched'],
false);
module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED;

View file

@ -21,9 +21,11 @@ const selectShowCommentComposer = state => selectIsSocial(state);
const selectCanReportComment = state => selectIsSocial(state);
const selectCanRestoreComment = state => selectIsAdmin(state);
// On the project page, project owners can delete comments with a confirmation,
// and admins can delete comments without a confirmation. For now, only admins
// can delete studio comments, so the following two are the same.
const selectCanDeleteComment = state => selectIsAdmin(state);
// and admins can delete comments without a confirmation.
// On the studio page, studio creators and managers have the ability to delete *their own* comments with confirmation.
// Admins can delete comments without a confirmation.
const selectCanDeleteAnyComment = state => selectIsAdmin(state);
const selectCanDeleteOwnComment = state => isCreator(state) || isManager(state);
const selectCanDeleteCommentWithoutConfirm = state => selectIsAdmin(state);
const selectCanFollowStudio = state => selectIsLoggedIn(state);
@ -80,7 +82,8 @@ export {
selectCanAddProjects,
selectCanFollowStudio,
selectShowCommentComposer,
selectCanDeleteComment,
selectCanDeleteAnyComment,
selectCanDeleteOwnComment,
selectCanDeleteCommentWithoutConfirm,
selectCanReportComment,
selectCanRestoreComment,

View file

@ -46,6 +46,5 @@
"project.cloudVariables": "Cloud Variables",
"project.cloudDataLink": "See Data",
"project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project.",
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful.",
"project.mutedAddToStudio": "You will be able to add to studios again {inDuration}."
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful."
}

View file

@ -160,6 +160,7 @@ $stage-width: 480px;
margin-top: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
@ -184,6 +185,7 @@ $stage-width: 480px;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;

View file

@ -8,133 +8,90 @@ const Button = require('../../components/forms/button.jsx');
const AddToStudioModal = require('./add-to-studio.jsx');
const SocialModal = require('../../components/modal/social/container.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
const {connect} = require('react-redux');
const {selectShowProjectMuteError} = require('../../redux/studio-permissions.js');
const {useState} = require('react');
const projectShape = require('./projectshape.jsx').projectShape;
import {selectNewStudiosLaunched} from '../../redux/session.js';
import StudioMuteEditMessage from '../studio/studio-mute-edit-message.jsx';
require('./subactions.scss');
const Subactions = ({
addToStudioOpen,
canAddToStudio,
canReport,
isAdmin,
isShared,
onAddToStudioClicked,
onAddToStudioClosed,
onReportClicked,
onReportClose,
onReportSubmit,
onSocialClicked,
onSocialClosed,
onToggleStudio,
projectInfo,
reportOpen,
shareDate,
showAddToStudioMuteError,
socialOpen,
userOwnsProject
}) => {
const [showMuteMessage, setShowMuteMessage] = useState(false);
return (
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{shareDate ? (
<FormattedDate
value={Date.parse(shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
) : 'Unshared'}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
{(canReport) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={onReportClicked}
>
<FormattedMessage id="general.report" />
</Button>
{reportOpen && (
<ReportModal
isOpen
key="report-modal"
type="project"
onReport={onReportSubmit}
onRequestClose={onReportClose}
/>
)}
</React.Fragment>
}
{canAddToStudio &&
<React.Fragment>
<div
style={{position: 'relative'}}
/* eslint-disable react/jsx-no-bind */
onMouseEnter={() => showAddToStudioMuteError && setShowMuteMessage(true)}
onMouseLeave={() => showAddToStudioMuteError && setShowMuteMessage(false)}
/* eslint-enable react/jsx-no-bind */
>
<Button
className="action-button studio-button"
disabled={showAddToStudioMuteError}
key="add-to-studio-button"
onClick={showMuteMessage ? null : onAddToStudioClicked}
>
<FormattedMessage id="addToStudio.title" />
</Button>
{showMuteMessage && <StudioMuteEditMessage
className="studio-button-error"
messageId="project.mutedAddToStudio"
/>}
</div>
{addToStudioOpen && (
<AddToStudioModal
isOpen
isAdmin={isAdmin}
key="add-to-studio-modal"
userOwnsProject={userOwnsProject}
onRequestClose={onAddToStudioClosed}
onToggleStudio={onToggleStudio}
/>
)}
</React.Fragment>
}
{/* only show copy link button, modal if project is shared */}
{isShared && projectInfo && projectInfo.id && (
<React.Fragment>
<Button
className="action-button copy-link-button"
onClick={onSocialClicked}
>
<FormattedMessage id="general.copyLink" />
</Button>
{socialOpen && (
<SocialModal
isOpen
key="social-modal"
projectId={projectInfo && projectInfo.id}
onRequestClose={onSocialClosed}
/>
)}
</React.Fragment>
)}
</FlexRow>
const Subactions = props => (
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{props.shareDate ? (
<FormattedDate
value={Date.parse(props.shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
) : 'Unshared'}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
{(props.canReport) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={props.onReportClicked}
>
<FormattedMessage id="general.report" />
</Button>
{props.reportOpen && (
<ReportModal
isOpen
key="report-modal"
type="project"
onReport={props.onReportSubmit}
onRequestClose={props.onReportClose}
/>
)}
</React.Fragment>
}
{props.canAddToStudio &&
<React.Fragment>
<Button
className="action-button studio-button"
key="add-to-studio-button"
onClick={props.onAddToStudioClicked}
>
<FormattedMessage id="addToStudio.title" />
</Button>
{props.addToStudioOpen && (
<AddToStudioModal
isOpen
isAdmin={props.isAdmin}
key="add-to-studio-modal"
userOwnsProject={props.userOwnsProject}
onRequestClose={props.onAddToStudioClosed}
onToggleStudio={props.onToggleStudio}
/>
)}
</React.Fragment>
}
{/* only show copy link button, modal if project is shared */}
{props.isShared && props.projectInfo && props.projectInfo.id && (
<React.Fragment>
<Button
className="action-button copy-link-button"
onClick={props.onSocialClicked}
>
<FormattedMessage id="general.copyLink" />
</Button>
{props.socialOpen && (
<SocialModal
isOpen
key="social-modal"
projectId={props.projectInfo && props.projectInfo.id}
onRequestClose={props.onSocialClosed}
/>
)}
</React.Fragment>
)}
</FlexRow>
);
};
</FlexRow>
);
Subactions.propTypes = {
addToStudioOpen: PropTypes.bool,
@ -153,13 +110,8 @@ Subactions.propTypes = {
projectInfo: projectShape,
reportOpen: PropTypes.bool,
shareDate: PropTypes.string,
showAddToStudioMuteError: PropTypes.bool,
socialOpen: PropTypes.bool,
userOwnsProject: PropTypes.bool
};
module.exports = connect(
state => ({
showAddToStudioMuteError: selectShowProjectMuteError(state) && selectNewStudiosLaunched(state)
})
)(Subactions);
module.exports = Subactions;

View file

@ -109,10 +109,3 @@
}
}
}
.studio-button-error {
top: auto;
transform: none;
width: 100%;
margin-left: 0;
}

View file

@ -10,10 +10,11 @@ import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import StudioCommentsAllowed from './studio-comments-allowed.jsx';
import StudioCommentsNotAllowed from './studio-comments-not-allowed.jsx';
import {selectIsAdmin, selectHasFetchedSession} from '../../redux/session';
import {selectIsAdmin, selectHasFetchedSession, selectUsername} from '../../redux/session';
import {
selectShowCommentComposer,
selectCanDeleteComment,
selectCanDeleteAnyComment,
selectCanDeleteOwnComment,
selectCanDeleteCommentWithoutConfirm,
selectCanReportComment,
selectCanRestoreComment,
@ -36,7 +37,9 @@ const StudioComments = ({
shouldShowCommentComposer,
shouldShowCommentsList,
shouldShowCommentsGloballyOffError,
canDeleteComment,
username,
canDeleteAnyComment,
canDeleteOwnComment,
canDeleteCommentWithoutConfirm,
canEditCommentsAllowed,
canReportComment,
@ -106,7 +109,7 @@ const StudioComments = ({
<TopLevelComment
hasThreadLimit
author={comment.author}
canDelete={canDeleteComment}
canDelete={canDeleteAnyComment || (canDeleteOwnComment && comment.author.username === username)}
canDeleteWithoutConfirm={canDeleteCommentWithoutConfirm}
canReply={shouldShowCommentComposer}
canReport={canReportComment}
@ -141,6 +144,7 @@ const StudioComments = ({
}
</div>
}
</div>
);
};
@ -157,7 +161,9 @@ StudioComments.propTypes = {
shouldShowCommentComposer: PropTypes.bool,
shouldShowCommentsGloballyOffError: PropTypes.bool,
shouldShowCommentsList: PropTypes.bool,
canDeleteComment: PropTypes.bool,
username: PropTypes.string,
canDeleteAnyComment: PropTypes.bool,
canDeleteOwnComment: PropTypes.bool,
canDeleteCommentWithoutConfirm: PropTypes.bool,
canEditCommentsAllowed: PropTypes.bool,
canReportComment: PropTypes.bool,
@ -181,11 +187,13 @@ export default connect(
isAdmin: selectIsAdmin(state),
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,
username: selectUsername(state),
commentsAllowed: selectStudioCommentsAllowed(state),
shouldShowCommentComposer: selectShowCommentComposer(state),
shouldShowCommentsGloballyOffError: selectShowCommentsGloballyOffError(state),
shouldShowCommentsList: selectShowCommentsList(state),
canDeleteComment: selectCanDeleteComment(state),
canDeleteAnyComment: selectCanDeleteAnyComment(state),
canDeleteOwnComment: selectCanDeleteOwnComment(state),
canDeleteCommentWithoutConfirm: selectCanDeleteCommentWithoutConfirm(state),
canEditCommentsAllowed: selectCanEditCommentsAllowed(state),
canReportComment: selectCanReportComment(state),

View file

@ -9,15 +9,12 @@ import {selectMuteStatus} from '../../redux/session';
import {formatRelativeTime} from '../../lib/format-time.js';
const StudioMuteEditMessage = ({
className,
messageId,
muteExpiresAtMs
}) => (
<ValidationMessage
className={className}
mode="info"
message={<FormattedMessage
id={messageId}
id="studio.mutedEdit"
values={{
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
}}
@ -27,15 +24,9 @@ const StudioMuteEditMessage = ({
StudioMuteEditMessage.propTypes = {
className: PropTypes.string,
messageId: PropTypes.string,
muteExpiresAtMs: PropTypes.number
};
StudioMuteEditMessage.defaultProps = {
messageId: 'studio.mutedEdit'
};
export default connect(
state => ({
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)

View file

@ -25,7 +25,7 @@
.header-wrapper {
width: 100%;
height: 100px;
height: 10vh;
}
.header-content {
width: calc(100% - 2rem);
@ -40,7 +40,6 @@
width: 90vw;
max-width: 45rem;
margin: 0 auto;
padding-top: 4vw;
text-align: center;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
color: white;
@ -59,9 +58,13 @@
opacity: 25%;
margin: 2rem 0;
}
img.main-img {
width: 300px;
height: 200px;
}
h1 {
font-size: 2rem;
margin: 1rem 1rem 2.5rem 1rem;
margin: 1.5rem 1rem 2.5rem 1rem;
}
h2 {
font-size: 1.125rem;
@ -239,12 +242,12 @@
</div>
<div class="main">
<div class="content">
<h1>The Scratch Team is working hard to fix an issue with the Scratch website</h1>
<h2 class="reload" onclick="location.reload()">Try reloading the page in a few minutes</h2>
<img src="http://scratch-maintenance.s3.amazonaws.com/cairn.svg" />
<h1>The Scratch Team is working on some planned updates to the website</h1>
<h2 class="comment">Check <a href="https://twitter.com/scratch">@Scratch</a> for updates</h2>
<hr>
<div class="narrow">
<p>While the website is being fixed, you can use the Scratch app to make projects offline</p>
<p>While the website is being worked on, you can use the Scratch app to make projects offline</p>
</div>
<button class="toggleShowing showing"><span>Download Scratch app</span></button>
<div class="grid-wrapper hidden">

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,8 @@ import {
selectCanEditInfo,
selectCanAddProjects,
selectShowCommentComposer,
selectCanDeleteComment,
selectCanDeleteAnyComment,
selectCanDeleteOwnComment,
selectCanDeleteCommentWithoutConfirm,
selectCanReportComment,
selectCanRestoreComment,
@ -198,7 +199,7 @@ describe('studio comments', () => {
});
});
describe('can delete comment', () => {
describe('can delete any comment', () => {
test.each([
['admin', true],
['curator', false],
@ -211,7 +212,26 @@ describe('studio comments', () => {
['muted logged in', false]
])('%s: %s', (role, expected) => {
setStateByRole(role);
expect(selectCanDeleteComment(state)).toBe(expected);
expect(selectCanDeleteAnyComment(state)).toBe(expected);
});
});
describe('can delete own comment', () => {
test.each([
['admin', false], // This is false here because we check for `canDeleteAnyComment` separately
['curator', false],
['manager', true],
['creator', true],
['logged in', false],
['unconfirmed', false],
['logged out', false],
['muted creator', true],
['muted manager', true],
['muted curator', false],
['muted logged in', false]
])('%s: %s', (role, expected) => {
setStateByRole(role);
expect(selectCanDeleteOwnComment(state)).toBe(expected);
});
});