diff --git a/src/lib/project-info.js b/src/lib/project-info.js new file mode 100644 index 000000000..8eee1389b --- /dev/null +++ b/src/lib/project-info.js @@ -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 + } +}; diff --git a/src/views/preview/l10n.json b/src/views/preview/l10n.json index 5935afaba..6d433058a 100644 --- a/src/views/preview/l10n.json +++ b/src/views/preview/l10n.json @@ -32,5 +32,7 @@ "project.numSprites": "{number} sprites", "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.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." } diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index 2ac0ff555..91bb3a725 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -105,6 +105,8 @@ const PreviewPresentation = ({ onUpdateProjectId, originalInfo, parentInfo, + showCloudDataAlert, + showUsernameBlockAlert, projectHost, projectId, projectInfo, @@ -267,6 +269,16 @@ const PreviewPresentation = ({
+ {showCloudDataAlert && ( + + + + )} + {showUsernameBlockAlert && ( + + + + )} 0) { - storage - .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) - .then(projectAsset => { // NOTE: this is turning up null, breaking the line below. - let input = projectAsset.data; - if (typeof input === 'object' && !(input instanceof ArrayBuffer) && - !ArrayBuffer.isView(input)) { // taken from scratch-vm - // If the input is an object and not any ArrayBuffer - // or an ArrayBuffer view (this includes all typed arrays and DataViews) - // turn the object into a JSON string, because we suspect - // this is a project.json as an object - // validate expects a string or buffer as input - // TODO not sure if we need to check that it also isn't a data view - input = JSON.stringify(input); - } - parser(projectAsset.data, false, (err, projectData) => { - if (err) { - log.error(`Unhandled project parsing error: ${err}`); - return; - } - const extensionSet = new Set(); - if (projectData[0].extensions) { - 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({ - extensions: Array.from(extensionSet), - modInfo: { - scriptCount: scriptCount, - spriteCount: spriteCount - } - }); - }); - }); - } else { // projectId is default or invalid; empty the extensions array - this.setState({ - extensions: [], - modInfo: { - scriptCount: 0, - spriteCount: 0 + getProjectData (projectId, showAlerts) { + if (projectId <= 0) return 0; + storage + .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) + .then(projectAsset => { // NOTE: this is turning up null, breaking the line below. + let input = projectAsset.data; + if (typeof input === 'object' && !(input instanceof ArrayBuffer) && + !ArrayBuffer.isView(input)) { // taken from scratch-vm + // If the input is an object and not any ArrayBuffer + // or an ArrayBuffer view (this includes all typed arrays and DataViews) + // turn the object into a JSON string, because we suspect + // this is a project.json as an object + // validate expects a string or buffer as input + // TODO not sure if we need to check that it also isn't a data view + input = JSON.stringify(input); } + parser(projectAsset.data, false, (err, projectData) => { + if (err) { + log.error(`Unhandled project parsing error: ${err}`); + return; + } + const newState = { + modInfo: {} // Filled in below + }; + + const helpers = ProjectInfo[projectData[0].projectVersion]; + if (!helpers) return; // sb1 not handled + newState.extensions = helpers.extensions(projectData[0]); + newState.modInfo.scriptCount = helpers.scriptCount(projectData[0]); + 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]); + } + } + this.setState(newState); + }); }); - } } handleToggleComments () { this.props.updateProject( @@ -354,6 +335,10 @@ class Preview extends React.Component { this.props.reportProject(this.state.projectId, formData, this.props.user.token); } handleGreenFlag () { + this.setState({ + showUsernameBlockAlert: false, + showCloudDataAlert: false + }); this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token); } handlePopState () { @@ -446,6 +431,10 @@ class Preview extends React.Component { this.props.remixProject(); } handleSeeInside () { + this.setState({ // Remove any project alerts so they don't show up later + showUsernameBlockAlert: false, + showCloudDataAlert: false + }); this.props.setPlayer(false); if (this.state.justRemixed || this.state.justShared) { this.setState({ @@ -601,7 +590,9 @@ class Preview extends React.Component { replies={this.props.replies} reportOpen={this.state.reportOpen} showAdminPanel={this.props.isAdmin} + showCloudDataAlert={this.state.showCloudDataAlert} showModInfo={this.props.isAdmin} + showUsernameBlockAlert={this.state.showUsernameBlockAlert} singleCommentId={this.state.singleCommentId} visibilityInfo={this.props.visibilityInfo} onAddComment={this.handleAddComment}