Merge pull request #2433 from paulkaplan/cloud-data-modal

Show username and cloud var alerts on projects that use them.
This commit is contained in:
Paul Kaplan 2018-12-11 16:58:11 -05:00 committed by GitHub
commit a99e8574f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 68 deletions

36
src/lib/project-info.js Normal file
View file

@ -0,0 +1,36 @@
const EXTENSION_INFO = require('./extensions.js').default;
module.exports = {
// Keys match the projectVersion key from project serialization
3: {
extensions: project => {
(project.extensions || []).map(ext => EXTENSION_INFO[ext])
.filter(ext => !!ext); // Only include extensions in the info lib
},
spriteCount: project =>
project.targets.length - 1, // don't count stage
scriptCount: project => project.targets
.map(target => Object.values(target.blocks))
.reduce((accumulator, currentVal) => accumulator.concat(currentVal), [])
.filter(block => block.topLevel).length,
usernameBlock: project => project.targets
.map(target => Object.values(target.blocks))
.reduce((accumulator, currentVal) => accumulator.concat(currentVal), [])
.some(block => block.opcode === 'sensing_username'),
cloudData: project => {
const stage = project.targets[0];
return Object.values(stage.variables)
.some(variable => variable.length === 3); // 3 entries if cloud var
}
},
2: {
extensions: () => [], // Showing extension chip not implemented for scratch2 projects
spriteCount: project => project.info.spriteCount,
scriptCount: project => project.info.scriptCount,
usernameBlock: project =>
// Block traversing is complicated in scratch2 projects...
// This check should work even if you have sprites named getUserName, etc.
JSON.stringify(project).indexOf('["getUserName"]') !== -1,
cloudData: project => project.info.hasCloudData
}
};

View file

@ -32,5 +32,7 @@
"project.numSprites": "{number} sprites", "project.numSprites": "{number} sprites",
"project.descriptionMaxLength": "Description is too long", "project.descriptionMaxLength": "Description is too long",
"project.notesPlaceholder": "How did you make this project? Did you use ideas, scripts or artwork from other people? Thank them here.", "project.notesPlaceholder": "How did you make this project? Did you use ideas, scripts or artwork from other people? Thank them here.",
"project.descriptionPlaceholder": "Tell people how to use your project (such as which keys to press)." "project.descriptionPlaceholder": "Tell people how to use your project (such as which keys to press).",
"project.cloudDataAlert": "This project uses cloud data - a feature that is only available to signed in users.",
"project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project."
} }

View file

@ -105,6 +105,8 @@ const PreviewPresentation = ({
onUpdateProjectId, onUpdateProjectId,
originalInfo, originalInfo,
parentInfo, parentInfo,
showCloudDataAlert,
showUsernameBlockAlert,
projectHost, projectHost,
projectId, projectId,
projectInfo, projectInfo,
@ -267,6 +269,16 @@ const PreviewPresentation = ({
</FlexRow> </FlexRow>
<FlexRow className="preview-row"> <FlexRow className="preview-row">
<div className="guiPlayer"> <div className="guiPlayer">
{showCloudDataAlert && (
<FlexRow className="project-info-alert">
<FormattedMessage id="project.cloudDataAlert" />
</FlexRow>
)}
{showUsernameBlockAlert && (
<FlexRow className="project-info-alert">
<FormattedMessage id="project.usernameBlockAlert" />
</FlexRow>
)}
<IntlGUI <IntlGUI
isPlayerOnly isPlayerOnly
assetHost={assetHost} assetHost={assetHost}
@ -647,7 +659,9 @@ PreviewPresentation.propTypes = {
replies: PropTypes.objectOf(PropTypes.array), replies: PropTypes.objectOf(PropTypes.array),
reportOpen: PropTypes.bool, reportOpen: PropTypes.bool,
showAdminPanel: PropTypes.bool, showAdminPanel: PropTypes.bool,
showCloudDataAlert: PropTypes.bool,
showModInfo: PropTypes.bool, showModInfo: PropTypes.bool,
showUsernameBlockAlert: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
visibilityInfo: PropTypes.shape({ visibilityInfo: PropTypes.shape({
censored: PropTypes.bool, censored: PropTypes.bool,

View file

@ -347,8 +347,21 @@ $stage-width: 480px;
.guiPlayer { .guiPlayer {
display: inline-block; display: inline-block;
position: relative;
width: $player-width; width: $player-width;
$alert-bg: rgba(255, 255, 255, .85);
.project-info-alert {
position: absolute;
z-index: 100;
margin: 60px 15px;
border-radius: .25rem;
background: $alert-bg;
padding: .75rem;
text-align: center;
font-size: .95rem;
}
@media #{$small} { @media #{$small} {
width: 100%; width: 100%;
} }

View file

@ -13,10 +13,9 @@ const copy = require('clipboard-copy');
const Page = require('../../components/page/www/page.jsx'); const Page = require('../../components/page/www/page.jsx');
const storage = require('../../lib/storage.js').default; const storage = require('../../lib/storage.js').default;
const log = require('../../lib/log'); const log = require('../../lib/log');
const EXTENSION_INFO = require('../../lib/extensions.js').default;
const jar = require('../../lib/jar.js'); const jar = require('../../lib/jar.js');
const thumbnailUrl = require('../../lib/user-thumbnail'); const thumbnailUrl = require('../../lib/user-thumbnail');
const ProjectInfo = require('../../lib/project-info');
const PreviewPresentation = require('./presentation.jsx'); const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx'); const Registration = require('../../components/registration/registration.jsx');
@ -102,6 +101,8 @@ class Preview extends React.Component {
scriptCount: 0, scriptCount: 0,
spriteCount: 0 spriteCount: 0
}, },
showCloudDataAlert: false,
showUsernameBlockAlert: false,
projectId: parts[1] === 'editor' ? '0' : parts[1], projectId: parts[1] === 'editor' ? '0' : parts[1],
reportOpen: false, reportOpen: false,
singleCommentId: singleCommentId singleCommentId: singleCommentId
@ -118,7 +119,7 @@ class Preview extends React.Component {
this.props.sessionStatus === sessionActions.Status.FETCHED) || this.props.sessionStatus === sessionActions.Status.FETCHED) ||
(this.state.projectId !== prevState.projectId))) { (this.state.projectId !== prevState.projectId))) {
this.fetchCommunityData(); this.fetchCommunityData();
this.getProjectData(this.state.projectId); this.getProjectData(this.state.projectId, true /* Show cloud/username alerts */);
if (this.state.justShared) { if (this.state.justShared) {
this.setState({ // eslint-disable-line react/no-did-update-set-state this.setState({ // eslint-disable-line react/no-did-update-set-state
justShared: false justShared: false
@ -155,7 +156,10 @@ class Preview extends React.Component {
// Switching out of editor mode, refresh data that comes from project json // Switching out of editor mode, refresh data that comes from project json
if (this.props.playerMode && !prevProps.playerMode) { if (this.props.playerMode && !prevProps.playerMode) {
this.getProjectData(this.state.projectId); this.getProjectData(
this.state.projectId,
false // Do not show cloud/username alerts again
);
} }
} }
componentWillUnmount () { componentWillUnmount () {
@ -215,8 +219,8 @@ class Preview extends React.Component {
} }
} }
} }
getProjectData (projectId) { getProjectData (projectId, showAlerts) {
if (projectId > 0) { if (projectId <= 0) return 0;
storage storage
.load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON)
.then(projectAsset => { // NOTE: this is turning up null, breaking the line below. .then(projectAsset => { // NOTE: this is turning up null, breaking the line below.
@ -236,50 +240,27 @@ class Preview extends React.Component {
log.error(`Unhandled project parsing error: ${err}`); log.error(`Unhandled project parsing error: ${err}`);
return; return;
} }
const extensionSet = new Set(); const newState = {
if (projectData[0].extensions) { modInfo: {} // Filled in below
projectData[0].extensions.forEach(extension => { };
const extensionInfo = EXTENSION_INFO[extension];
if (extensionInfo) {
extensionSet.add(extensionInfo);
}
});
}
let spriteCount = 0;
let scriptCount = 0;
if (projectData[0].projectVersion) {
if (projectData[0].projectVersion === 3) {
spriteCount = projectData[0].targets.length - 1; // don't count stage
scriptCount = projectData[0].targets
.map(target =>
Object.values(target.blocks)
.filter(block => block.topLevel).length
)
.reduce((accumulator, currentVal) => accumulator + currentVal, 0);
} else if (projectData[0].projectVersion === 2) { // sb2 file
spriteCount = projectData[0].info.spriteCount;
scriptCount = projectData[0].info.scriptCount;
}
} // else sb (scratch 1.x) numbers will be zero
this.setState({ const helpers = ProjectInfo[projectData[0].projectVersion];
extensions: Array.from(extensionSet), if (!helpers) return; // sb1 not handled
modInfo: { newState.extensions = helpers.extensions(projectData[0]);
scriptCount: scriptCount, newState.modInfo.scriptCount = helpers.scriptCount(projectData[0]);
spriteCount: spriteCount newState.modInfo.spriteCount = helpers.spriteCount(projectData[0]);
if (showAlerts) {
// Check for username block only if user is logged in
if (this.props.isLoggedIn) {
newState.showUsernameBlockAlert = helpers.usernameBlock(projectData[0]);
} else { // Check for cloud vars only if user is logged out
newState.showCloudDataAlert = helpers.cloudData(projectData[0]);
} }
});
});
});
} else { // projectId is default or invalid; empty the extensions array
this.setState({
extensions: [],
modInfo: {
scriptCount: 0,
spriteCount: 0
} }
this.setState(newState);
});
}); });
}
} }
handleToggleComments () { handleToggleComments () {
this.props.updateProject( this.props.updateProject(
@ -354,6 +335,10 @@ class Preview extends React.Component {
this.props.reportProject(this.state.projectId, formData, this.props.user.token); this.props.reportProject(this.state.projectId, formData, this.props.user.token);
} }
handleGreenFlag () { handleGreenFlag () {
this.setState({
showUsernameBlockAlert: false,
showCloudDataAlert: false
});
this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token); this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token);
} }
handlePopState () { handlePopState () {
@ -446,6 +431,10 @@ class Preview extends React.Component {
this.props.remixProject(); this.props.remixProject();
} }
handleSeeInside () { handleSeeInside () {
this.setState({ // Remove any project alerts so they don't show up later
showUsernameBlockAlert: false,
showCloudDataAlert: false
});
this.props.setPlayer(false); this.props.setPlayer(false);
if (this.state.justRemixed || this.state.justShared) { if (this.state.justRemixed || this.state.justShared) {
this.setState({ this.setState({
@ -601,7 +590,9 @@ class Preview extends React.Component {
replies={this.props.replies} replies={this.props.replies}
reportOpen={this.state.reportOpen} reportOpen={this.state.reportOpen}
showAdminPanel={this.props.isAdmin} showAdminPanel={this.props.isAdmin}
showCloudDataAlert={this.state.showCloudDataAlert}
showModInfo={this.props.isAdmin} showModInfo={this.props.isAdmin}
showUsernameBlockAlert={this.state.showUsernameBlockAlert}
singleCommentId={this.state.singleCommentId} singleCommentId={this.state.singleCommentId}
visibilityInfo={this.props.visibilityInfo} visibilityInfo={this.props.visibilityInfo}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}