Merge pull request #3441 from LLK/release/2019-10-10

[MASTER] Release 2019-10-10
This commit is contained in:
DD Liu 2019-10-10 16:23:23 -04:00 committed by GitHub
commit c8475d5a3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1240 additions and 795 deletions

View file

@ -24,7 +24,7 @@ var routes = routeJson.map(function (route) {
async.auto({ async.auto({
version: function (cb) { version: function (cb) {
fastly.getLatestVersion(function (err, response) { fastly.getLatestActiveVersion(function (err, response) {
if (err) return cb(err); if (err) return cb(err);
// Validate latest version before continuing // Validate latest version before continuing
if (response.active || response.locked) { if (response.active || response.locked) {

View file

@ -24,11 +24,11 @@ module.exports = function (apiKey, serviceId) {
}; };
/* /*
* getLatestVersion: Get the most recent version for the configured service * getLatestActiveVersion: Get the most recent version for the configured service
* *
* @param {callback} Callback with signature *err, latestVersion) * @param {callback} Callback with signature *err, latestVersion)
*/ */
fastly.getLatestVersion = function (cb) { fastly.getLatestActiveVersion = function (cb) {
if (!this.serviceId) { if (!this.serviceId) {
return cb('Failed to get latest version. No serviceId configured'); return cb('Failed to get latest version. No serviceId configured');
} }
@ -37,11 +37,15 @@ module.exports = function (apiKey, serviceId) {
if (err) { if (err) {
return cb('Failed to fetch versions: ' + err); return cb('Failed to fetch versions: ' + err);
} }
var latestVersion = versions.reduce(function (lateVersion, version) { var latestVersion = versions.reduce((latestActiveSoFar, cur) => {
if (!lateVersion) return version; // if one of [latestActiveSoFar, cur] is active and the other isn't,
if (version.number > lateVersion.number) return version; // return whichever is active. If both are not active, return
return lateVersion; // latestActiveSoFar.
}); if (!cur || !cur.active) return latestActiveSoFar;
if (!latestActiveSoFar || !latestActiveSoFar.active) return cur;
// when both are active, prefer whichever has a higher version number.
return (cur.number > latestActiveSoFar.number) ? cur : latestActiveSoFar;
}, null);
return cb(null, latestVersion); return cb(null, latestVersion);
}); });
}; };

1260
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,7 +59,7 @@
"autoprefixer": "6.3.6", "autoprefixer": "6.3.6",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-core": "6.23.1", "babel-core": "6.23.1",
"babel-eslint": "10.0.2", "babel-eslint": "10.0.3",
"babel-loader": "7.1.0", "babel-loader": "7.1.0",
"babel-plugin-transform-object-rest-spread": "6.26.0", "babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-require-context": "0.1.1", "babel-plugin-transform-require-context": "0.1.1",
@ -126,7 +126,7 @@
"redux-mock-store": "^1.2.3", "redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20190926141105", "scratch-gui": "0.1.0-prerelease.20191010184958",
"scratch-l10n": "latest", "scratch-l10n": "latest",
"selenium-webdriver": "3.6.0", "selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",

View file

@ -44,7 +44,7 @@ const ExtensionRequirements = props => (
alt="" alt=""
src="svgs/extensions/android.svg" src="svgs/extensions/android.svg"
/> />
Android 5.0+ Android 6.0+
</span> </span>
</React.Fragment> </React.Fragment>
)} )}

View file

@ -135,7 +135,7 @@ const Footer = props => (
</dd> </dd>
<dd> <dd>
<a href="/download"> <a href="/download">
<FormattedMessage id="general.offlineEditor" /> <FormattedMessage id="general.download" />
</a> </a>
</dd> </dd>
<dd> <dd>

View file

@ -26,3 +26,8 @@ input[type="checkbox"].formik-checkbox {
background-position: center; background-position: center;
} }
} }
.formik-checkbox-label {
padding-top: .0625rem;
display: block;
}

View file

@ -18,6 +18,7 @@
overflow: visible; overflow: visible;
color: $type-white; color: $type-white;
z-index: 1; z-index: 1;
font-weight: 500;
&:before { &:before {
display: block; display: block;
@ -82,7 +83,6 @@
.validation-info { .validation-info {
background-color: $ui-blue; background-color: $ui-blue;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15); box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15);
font-weight: 500;
&:before { &:before {
background-color: $ui-blue; background-color: $ui-blue;

View file

@ -46,11 +46,16 @@ const InstallScratch = ({
> >
<span className="step-description"> <span className="step-description">
<React.Fragment> <React.Fragment>
{isDownloaded(currentOS) && ( {currentOS === OS_ENUM.WINDOWS && (
<FormattedMessage <FormattedMessage
id={CHROME_APP_RELEASED ? 'installScratch.downloadScratchAppGeneric' : id={CHROME_APP_RELEASED ? 'installScratch.getScratchAppWindows' :
'installScratch.downloadScratchDesktop'}
/>
)}
{currentOS === OS_ENUM.MACOS && (
<FormattedMessage
id={CHROME_APP_RELEASED ? 'installScratch.getScratchAppMacOs' :
'installScratch.downloadScratchDesktop'} 'installScratch.downloadScratchDesktop'}
values={{operatingsystem: currentOS}}
/> />
)} )}
{isFromGooglePlay(currentOS) && ( {isFromGooglePlay(currentOS) && (

View file

@ -174,7 +174,7 @@ class EmailStep extends React.Component {
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('email')} onBlur={() => validateField('email')}
onChange={e => { onChange={e => {
setFieldValue('email', e.target.value); setFieldValue('email', e.target.value.substring(0, 254));
setFieldTouched('email'); setFieldTouched('email');
setFieldError('email', null); setFieldError('email', null);
}} }}

View file

@ -150,8 +150,8 @@ class GenderStep extends React.Component {
value={values.custom} value={values.custom}
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
onSetCustom={newCustomVal => setValues({ onSetCustom={newCustomVal => setValues({
gender: newCustomVal, gender: newCustomVal.substring(0, 25),
custom: newCustomVal custom: newCustomVal.substring(0, 25)
})} })}
onSetCustomRef={this.handleSetCustomRef} onSetCustomRef={this.handleSetCustomRef}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */

View file

@ -42,10 +42,16 @@
margin: 0; margin: 0;
border-top-left-radius: 1rem; border-top-left-radius: 1rem;
border-top-right-radius: 1rem; border-top-right-radius: 1rem;
/* match the small window setting for modal as a whole */
@media #{$small}, #{$small-height} {
border-top-left-radius: 0rem;
border-top-right-radius: 0rem;
}
} }
.join-flow-header-image { .join-flow-header-image {
width: 27.5rem; width: 100%;
} }
.join-flow-footer-message { .join-flow-footer-message {
@ -53,7 +59,11 @@
padding: 0.875rem 1.5rem; padding: 0.875rem 1.5rem;
background-color: $ui-blue-25percent; background-color: $ui-blue-25percent;
font-size: .75rem; font-size: .75rem;
font-weight: 600; font-weight: 500;
text-align: center; text-align: center;
color: $ui-blue; color: $ui-blue;
} }
.join-flow-footer-message a {
font-weight: 500;
}

View file

@ -136,9 +136,8 @@
} }
.join-flow-inner-email-step { .join-flow-inner-email-step {
padding-top: 1.25rem; padding-top: .75rem;
padding-bottom: 0; padding-bottom: 0;
min-height: 16.625rem;
} }
.join-flow-inner-welcome-step { .join-flow-inner-welcome-step {
@ -217,7 +216,7 @@
.join-flow-email-checkbox-row { .join-flow-email-checkbox-row {
font-size: .75rem; font-size: .75rem;
margin: 1.5rem .125rem .25rem; margin: 1.5rem .125rem 1rem;
} }
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active { a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {

View file

@ -16,14 +16,14 @@ const NextStepButton = props => (
type="submit" type="submit"
{...omit(props, ['intl', 'text', 'waiting'])} {...omit(props, ['intl', 'text', 'waiting'])}
> >
{props.waiting ? {props.waiting ? (
<Spinner /> : ( <Spinner className="next-step-spinner" />
<ModalTitle ) : (
className="next-step-title" <ModalTitle
title={props.content ? props.content : props.intl.formatMessage({id: 'general.next'})} className="next-step-title"
/> title={props.content ? props.content : props.intl.formatMessage({id: 'general.next'})}
) />
} )}
</button> </button>
); );

View file

@ -14,6 +14,12 @@
transition: background-color .25s ease; transition: background-color .25s ease;
background-color: $ui-orange-90percent; background-color: $ui-orange-90percent;
} }
/* match the small window setting for modal as a whole */
@media #{$small}, #{$small-height} {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
} }
.next-step-title { .next-step-title {
@ -22,3 +28,8 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.next-step-spinner {
width: 2.625rem;
height: 2.625rem;
}

View file

@ -20,7 +20,6 @@ class UsernameStep extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleChangeShowPassword',
'handleFocused', 'handleFocused',
'handleSetUsernameRef', 'handleSetUsernameRef',
'handleValidSubmit', 'handleValidSubmit',
@ -30,17 +29,13 @@ class UsernameStep extends React.Component {
'validateForm' 'validateForm'
]); ]);
this.state = { this.state = {
focused: null, focused: null
showPassword: false
}; };
} }
componentDidMount () { componentDidMount () {
// automatically start with focus on username field // automatically start with focus on username field
if (this.usernameInput) this.usernameInput.focus(); if (this.usernameInput) this.usernameInput.focus();
} }
handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword});
}
// track the currently focused input field, to determine whether each field should // track the currently focused input field, to determine whether each field should
// display a tooltip. (We only display it if a field is focused and has never been touched.) // display a tooltip. (We only display it if a field is focused and has never been touched.)
handleFocused (fieldName) { handleFocused (fieldName) {
@ -114,7 +109,7 @@ class UsernameStep extends React.Component {
username: '', username: '',
password: '', password: '',
passwordConfirm: '', passwordConfirm: '',
showPassword: false showPassword: true
}} }}
validate={this.validateForm} validate={this.validateForm}
validateOnBlur={false} validateOnBlur={false}
@ -162,7 +157,7 @@ class UsernameStep extends React.Component {
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')} onBlur={() => validateField('username')}
onChange={e => { onChange={e => {
setFieldValue('username', e.target.value); setFieldValue('username', e.target.value.substring(0, 30));
setFieldTouched('username'); setFieldTouched('username');
setFieldError('username', null); setFieldError('username', null);
}} }}
@ -192,7 +187,7 @@ class UsernameStep extends React.Component {
validationClassName="validation-full-width-input" validationClassName="validation-full-width-input"
onBlur={() => validateField('password')} onBlur={() => validateField('password')}
onChange={e => { onChange={e => {
setFieldValue('password', e.target.value); setFieldValue('password', e.target.value.substring(0, 128));
setFieldTouched('password'); setFieldTouched('password');
setFieldError('password', null); setFieldError('password', null);
}} }}
@ -230,20 +225,19 @@ class UsernameStep extends React.Component {
validationClassName="validation-full-width-input" validationClassName="validation-full-width-input"
onBlur={() => validateField('passwordConfirm')} onBlur={() => validateField('passwordConfirm')}
onChange={e => { onChange={e => {
setFieldValue('passwordConfirm', e.target.value); setFieldValue('passwordConfirm', e.target.value.substring(0, 128));
setFieldTouched('passwordConfirm'); setFieldTouched('passwordConfirm');
setFieldError('passwordConfirm', null); setFieldError('passwordConfirm', null);
}} }}
onFocus={() => this.handleFocused('passwordConfirm')} onFocus={() => this.handleFocused('passwordConfirm')}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */
/> />
<div className="join-flow-input-title"> <FormikCheckbox
<FormikCheckbox id="showPassword"
id="showPassword" label={this.props.intl.formatMessage({id: 'registration.showPassword'})}
label={this.props.intl.formatMessage({id: 'registration.showPassword'})} labelClassName="join-flow-input-title"
name="showPassword" name="showPassword"
/> />
</div>
</div> </div>
</div> </div>
</JoinFlowStep> </JoinFlowStep>

View file

@ -1,6 +1,32 @@
@import "../../../colors"; @import "../../../colors";
@import "../../../frameless"; @import "../../../frameless";
/* unusually for a modal, the join flow modal cares about the screen around it
being clickable, because of the standalone join view. */
.mod-join { .mod-join {
width: 27.4375rem; width: 27.4375rem;
@media #{$small} {
width: auto;
}
}
/* enable vertical scrolling when modal showing, if page is short */
.modal-overlay {
@media #{$small-height}, #{$medium-height} {
overflow: auto;
}
}
.modal-content {
@media #{$small}, #{$small-height} {
height: unset;
}
}
/* lower the modal slightly to accomodate Scratch logo above it */
.modal-sizes {
@media #{$small}, #{$small-height}, #{$medium}, #{$medium-height} {
margin: 3.5rem auto;
}
} }

View file

@ -18,7 +18,6 @@ const LoginDropdown = require('../../login/login-dropdown.jsx');
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx'); const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
const NavigationBox = require('../base/navigation.jsx'); const NavigationBox = require('../base/navigation.jsx');
const Registration = require('../../registration/registration.jsx'); const Registration = require('../../registration/registration.jsx');
const Scratch3Registration = require('../../registration/scratch3-registration.jsx');
const AccountNav = require('./accountnav.jsx'); const AccountNav = require('./accountnav.jsx');
require('./navigation.scss'); require('./navigation.scss');
@ -28,6 +27,7 @@ class Navigation extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'getProfileUrl', 'getProfileUrl',
'handleClickRegistration',
'handleSearchSubmit' 'handleSearchSubmit'
]); ]);
this.state = { this.state = {
@ -78,6 +78,13 @@ class Navigation extends React.Component {
if (!this.props.user) return; if (!this.props.user) return;
return `/users/${this.props.user.username}/`; return `/users/${this.props.user.username}/`;
} }
handleClickRegistration (event) {
if (this.props.useScratch3Registration) {
this.props.navigateToRegistration(event);
} else {
this.props.handleOpenRegistration(event);
}
}
handleSearchSubmit (formData) { handleSearchSubmit (formData) {
let targetUrl = '/search/projects'; let targetUrl = '/search/projects';
if (formData.q) { if (formData.q) {
@ -189,9 +196,12 @@ class Navigation extends React.Component {
className="link right join" className="link right join"
key="join" key="join"
> >
{/* there's no css class registrationLink -- this is
just to make the link findable for testing */}
<a <a
className="registrationLink"
href="#" href="#"
onClick={this.props.handleOpenRegistration} onClick={this.handleClickRegistration}
> >
<FormattedMessage id="general.joinScratch" /> <FormattedMessage id="general.joinScratch" />
</a> </a>
@ -214,18 +224,10 @@ class Navigation extends React.Component {
</li> </li>
]) : [] ]) : []
} }
{this.props.registrationOpen && ( {this.props.registrationOpen && !this.props.useScratch3Registration && (
this.props.useScratch3Registration ? ( <Registration
<Scratch3Registration key="registration"
createProjectOnComplete />
isOpen
key="scratch3registration"
/>
) : (
<Registration
key="registration"
/>
)
)} )}
</ul> </ul>
<CanceledDeletionModal /> <CanceledDeletionModal />
@ -243,6 +245,7 @@ Navigation.propTypes = {
handleToggleAccountNav: PropTypes.func, handleToggleAccountNav: PropTypes.func,
handleToggleLoginOpen: PropTypes.func, handleToggleLoginOpen: PropTypes.func,
intl: intlShape, intl: intlShape,
navigateToRegistration: PropTypes.func,
permissions: PropTypes.shape({ permissions: PropTypes.shape({
admin: PropTypes.bool, admin: PropTypes.bool,
social: PropTypes.bool, social: PropTypes.bool,
@ -305,14 +308,24 @@ const mapDispatchToProps = dispatch => ({
event.preventDefault(); event.preventDefault();
dispatch(navigationActions.toggleLoginOpen()); dispatch(navigationActions.toggleLoginOpen());
}, },
navigateToRegistration: event => {
event.preventDefault();
navigationActions.navigateToRegistration();
},
setMessageCount: newCount => { setMessageCount: newCount => {
dispatch(messageCountActions.setCount(newCount)); dispatch(messageCountActions.setCount(newCount));
} }
}); });
// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
{}, stateProps, dispatchProps, ownProps
);
const ConnectedNavigation = connect( const ConnectedNavigation = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps,
mergeProps
)(Navigation); )(Navigation);
module.exports = injectIntl(ConnectedNavigation); module.exports = injectIntl(ConnectedNavigation);

View file

@ -2,18 +2,13 @@
/* This class can be used on an icon that should spin. /* This class can be used on an icon that should spin.
It first plays the intro animation, then spins forever. */ It first plays the intro animation, then spins forever. */
animation-name: intro, spin; animation-name: intro, spin;
animation-duration: .25s, .5s; animation-duration: .25s, 1s;
animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear; animation-timing-function: cubic-bezier(.3, -3, .6, 3), cubic-bezier(0.4, 0.1, 0.4, 1);
animation-delay: 0s, .25s; animation-delay: 0s, .25s;
animation-iteration-count: 1, infinite; animation-iteration-count: 1, infinite;
animation-direction: normal; animation-direction: normal;
width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */ width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
height: 1.4rem; height: 1.4rem;
-webkit-animation-name: intro, spin;
-webkit-animation-duration: .25s, .5s;
-webkit-animation-iteration-count: 1, infinite;
-webkit-animation-delay: 0s, .25s;
-webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
transform-origin: center; transform-origin: center;
} }
@ -21,24 +16,20 @@
0% { 0% {
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
-webkit-transform: scale(0);
} }
100% { 100% {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
-webkit-transform: scale(1);
} }
} }
@keyframes spin { @keyframes spin {
0% { 0% {
transform: rotate(0); transform: rotate(0);
-webkit-transform: rotate(0);
} }
100% { 100% {
transform: rotate(359deg); transform: rotate(359deg);
-webkit-transform: rotate(359deg);
} }
} }

View file

@ -66,7 +66,7 @@
"general.notRequired": "Not Required", "general.notRequired": "Not Required",
"general.okay": "Okay", "general.okay": "Okay",
"general.other": "Other", "general.other": "Other",
"general.offlineEditor": "Offline Editor", "general.download": "Download",
"general.password": "Password", "general.password": "Password",
"general.press": "Press", "general.press": "Press",
"general.privacyPolicy": "Privacy Policy", "general.privacyPolicy": "Privacy Policy",
@ -137,12 +137,14 @@
"installScratch.or": "or", "installScratch.or": "or",
"installScratch.directDownload": "Direct download", "installScratch.directDownload": "Direct download",
"installScratch.desktopHeaderTitle": "Install Scratch Desktop", "installScratch.desktopHeaderTitle": "Install Scratch Desktop",
"installScratch.appHeaderTitle": "Install Scratch for {operatingsystem}", "installScratch.appHeaderTitle": "Install the Scratch app for {operatingsystem}",
"installScratch.downloadScratchDesktop": "Download Scratch Desktop", "installScratch.downloadScratchDesktop": "Download Scratch Desktop",
"installScratch.downloadScratchAppGeneric": "Download Scratch for {operatingsystem}", "installScratch.downloadScratchAppGeneric": "Download Scratch for {operatingsystem}",
"installScratch.getScratchAppPlay": "Get Scratch on the Google Play Store", "installScratch.getScratchAppPlay": "Get the Scratch app on the Google Play Store",
"installScratch.getScratchAppMacOs": "Get the Scratch app on the Mac App Store",
"installScratch.getScratchAppWindows": "Get the Scratch app on the Microsoft Store",
"installScratch.useScratchApp": "Open the Scratch app on your device.", "installScratch.useScratchApp": "Open the Scratch app on your device.",
"installScratchLink.installHeaderTitle": "Install Scratch Link", "installScratchLink.installHeaderTitle": "Install Scratch Link",
"installScratchLink.downloadAndInstall": "Download and install Scratch Link.", "installScratchLink.downloadAndInstall": "Download and install Scratch Link.",
"installScratchLink.startScratchLink": "Start Scratch Link and make sure it is running. It should appear in your toolbar.", "installScratchLink.startScratchLink": "Start Scratch Link and make sure it is running. It should appear in your toolbar.",

18
src/lib/sentry.js Normal file
View file

@ -0,0 +1,18 @@
const initSentry = () => {
// initialize Sentry instance, making sure it hasn't been initialized already
if (!window.Sentry && `${process.env.SENTRY_DSN}` !== '') {
const Sentry = require('@sentry/browser');
Sentry.init({
dsn: `${process.env.SENTRY_DSN}`,
// Do not collect global onerror, only collect specifically from React error boundaries.
// TryCatch plugin also includes errors from setTimeouts (i.e. the VM)
integrations: integrations => integrations.filter(i =>
!(i.name === 'GlobalHandlers' || i.name === 'TryCatch'))
});
window.Sentry = Sentry; // Allow GUI access to Sentry via window
}
};
module.exports = initSentry;

View file

@ -92,6 +92,10 @@ module.exports.setSearchTerm = searchTerm => ({
searchTerm: searchTerm searchTerm: searchTerm
}); });
module.exports.navigateToRegistration = () => {
window.location = '/join';
};
module.exports.handleCompleteRegistration = createProject => (dispatch => { module.exports.handleCompleteRegistration = createProject => (dispatch => {
if (createProject) { if (createProject) {
window.location = '/projects/editor/?tutorial=getStarted'; window.location = '/projects/editor/?tutorial=getStarted';

View file

@ -177,12 +177,20 @@
}, },
{ {
"name": "projects", "name": "projects",
"pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen|/embed)?)?)?/?(\\?.*)?$", "pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen)?)?)?/?(\\?.*)?$",
"routeAlias": "/projects/?$", "routeAlias": "/projects/?$",
"view": "preview/preview", "view": "preview/preview",
"title": "Scratch Project", "title": "Scratch Project",
"dynamicMetaTags": true "dynamicMetaTags": true
}, },
{
"name": "embed",
"pattern": "^/projects/\\d+/embed/?(\\?.*)?$",
"routeAlias": "/projects/?$",
"view": "preview/embed",
"title": "Scratch Project",
"dynamicMetaTags": true
},
{ {
"name": "parents", "name": "parents",
"pattern": "^/parents/?(\\?.*)?$", "pattern": "^/parents/?(\\?.*)?$",

View file

@ -95,7 +95,7 @@ class Download extends React.Component {
alt="" alt=""
src="svgs/extensions/android.svg" src="svgs/extensions/android.svg"
/> />
Android 5.0+ Android 6.0+
</span> </span>
</React.Fragment> </React.Fragment>
)} )}
@ -181,7 +181,17 @@ class Download extends React.Component {
<FormattedMessage id="download.troubleshootingTitle" /> <FormattedMessage id="download.troubleshootingTitle" />
</h2> </h2>
{isDownloaded(this.state.OS) && ( {CHROME_APP_RELEASED && (
<React.Fragment>
<h3 className="faq-question">
<FormattedMessage id="download.doIHaveToDownload" />
</h3>
<p>
<FormattedMessage id="download.doIHaveToDownloadAnswer" />
</p>
</React.Fragment>
)}
{!CHROME_APP_RELEASED && (
<React.Fragment> <React.Fragment>
<h3 className="faq-question"> <h3 className="faq-question">
<FormattedMessage id="download.canIUseScratchLink" /> <FormattedMessage id="download.canIUseScratchLink" />
@ -191,25 +201,29 @@ class Download extends React.Component {
</p> </p>
</React.Fragment> </React.Fragment>
)} )}
{isFromGooglePlay(this.state.OS) && ( {CHROME_APP_RELEASED && (
<React.Fragment> <React.Fragment>
<h3 className="faq-question"> <h3 className="faq-question">
<FormattedMessage id="download.canIUseExtensions" /> <FormattedMessage id="download.howConnectHardwareDevices" />
</h3> </h3>
<p> {isDownloaded(this.state.OS) && (
<FormattedMessage id="download.canIUseExtensionsAnswer" /> <p>
</p> <FormattedMessage
id="download.howConnectHardwareDevicesAnswerLink"
values={{operatingsystem: this.state.OS}}
/>
</p>
)}
{isFromGooglePlay(this.state.OS) && (
<p>
<FormattedMessage
id="download.howConnectHardwareDevicesAnswerApp"
values={{operatingsystem: this.state.OS}}
/>
</p>
)}
</React.Fragment> </React.Fragment>
)} )}
<h3 className="faq-question">
{isFromGooglePlay(this.state.OS) ?
<FormattedMessage id="download.appAndBrowser" /> :
<FormattedMessage id="download.desktopAndBrowser" />
}
</h3>
<p>
<FormattedMessage id="download.yesAnswer" />
</p>
{isDownloaded(this.state.OS) && (CHROME_APP_RELEASED ? ( {isDownloaded(this.state.OS) && (CHROME_APP_RELEASED ? (
<React.Fragment> <React.Fragment>
<h3 className="faq-question"> <h3 className="faq-question">
@ -248,6 +262,25 @@ class Download extends React.Component {
</p> </p>
</React.Fragment> </React.Fragment>
)} )}
<h3 className="faq-question">
{CHROME_APP_RELEASED ?
<FormattedMessage id="download.appAndBrowser" /> :
<FormattedMessage id="download.desktopAndBrowser" />
}
</h3>
<p>
<FormattedMessage id="download.yesAnswer" />
</p>
{CHROME_APP_RELEASED && (
<React.Fragment>
<h3 className="faq-question">
<FormattedMessage id="download.onPhone" />
</h3>
<p>
<FormattedMessage id="download.onPhoneAnswer" />
</p>
</React.Fragment>
)}
{!CHROME_APP_RELEASED && ( {!CHROME_APP_RELEASED && (
<React.Fragment> <React.Fragment>
<h3 className="faq-question"> <h3 className="faq-question">
@ -259,10 +292,16 @@ class Download extends React.Component {
</React.Fragment> </React.Fragment>
)} )}
<h3 className="faq-question"> <h3 className="faq-question">
<FormattedMessage id="download.whenSupportLinux" /> {CHROME_APP_RELEASED ?
<FormattedMessage id="download.whenSupportLinuxApp" /> :
<FormattedMessage id="download.whenSupportLinux" />
}
</h3> </h3>
<p> <p>
<FormattedMessage id="download.supportLinuxAnswer" /> {CHROME_APP_RELEASED ?
<FormattedMessage id="download.whenSupportLinuxAppAnswer" /> :
<FormattedMessage id="download.supportLinuxAnswer" />
}
</p> </p>
</FlexRow> </FlexRow>
</div> </div>

View file

@ -56,7 +56,7 @@
.download-info { .download-info {
padding-right: $cols1; padding-right: $cols1;
max-width: $cols6 + ($gutter / $em); max-width: $cols7 + ($gutter / $em);
align-items: flex-start; align-items: flex-start;
.download-copy { .download-copy {
@ -110,7 +110,7 @@
.download-image { .download-image {
width: 100%; width: 100%;
max-width: $cols6; max-width: $cols5;
img { img {
max-width: 100%; max-width: 100%;

View file

@ -1,35 +1,44 @@
{ {
"download.title": "Scratch Desktop", "download.title": "Scratch Desktop",
"download.intro": "You can install the Scratch Desktop editor to work on projects without an internet connection. This version will work on Windows and MacOS.", "download.intro": "You can install the Scratch Desktop editor to work on projects without an internet connection. This version will work on Windows and MacOS.",
"download.appTitle": "Download Scratch", "download.appTitle": "Download the Scratch App",
"download.appIntro": "You can install Scratch for free to work on projects without an internet connection.", "download.appIntro": "Would you like to create and save Scratch projects without an internet connection? Download the free Scratch app.",
"download.requirements": "Requirements", "download.requirements": "Requirements",
"download.imgAltDownloadIllustration" : "Scratch 3.0 Desktop screenshot", "download.imgAltDownloadIllustration" : "Scratch 3.0 Desktop screenshot",
"download.troubleshootingTitle": "FAQ", "download.troubleshootingTitle": "FAQ",
"download.startScratchDesktop": "Start Scratch Desktop", "download.startScratchDesktop": "Start Scratch Desktop",
"download.howDoIInstall": "How do I install Scratch Desktop?", "download.howDoIInstall": "How do I install Scratch Desktop?",
"download.whenSupportLinuxApp": "When will you have the Scratch app available for Linux?",
"download.whenSupportLinux": "When will you have Scratch Desktop for Linux?", "download.whenSupportLinux": "When will you have Scratch Desktop for Linux?",
"download.supportLinuxAnswer" : "Scratch Desktop on Linux is currently not supported. We are working with partners and the open-source community to determine if there is a way we can support Linux in the future. Stay tuned!", "download.supportLinuxAnswer" : "Scratch Desktop on Linux is currently not supported. We are working with partners and the open-source community to determine if there is a way we can support Linux in the future. Stay tuned!",
"download.whenSupportLinuxAppAnswer" : "The Scratch app is currently not supported on Linux. We are working with partners and the open-source community to determine if there is a way we can support Linux in the future. Stay tuned!",
"download.supportChromeOS" : "When will you have Scratch Desktop for Chromebooks?", "download.supportChromeOS" : "When will you have Scratch Desktop for Chromebooks?",
"download.supportChromeOSAnswer": "Scratch Desktop for Chromebooks is not yet available. We are working on it and expect to release later in 2019.", "download.supportChromeOSAnswer": "Scratch Desktop for Chromebooks is not yet available. We are working on it and expect to release later in 2019.",
"download.olderVersionsTitle" : "Older Versions", "download.olderVersionsTitle" : "Older Versions",
"download.olderVersions": "Looking for earlier Scratch Offline Editors?", "download.olderVersions": "Looking for an earlier version of Scratch?",
"download.scratch1-4Desktop" : "Scratch 1.4", "download.scratch1-4Desktop" : "Scratch 1.4",
"download.scratch2Desktop" : "Scratch 2.0 Offline Editor", "download.scratch2Desktop" : "Scratch 2.0 Offline Editor",
"download.cannotAccessMacStore" : "What if I can't access the Mac App Store?", "download.cannotAccessMacStore" : "What if I can't access the Mac App Store?",
"download.cannotAccessWindowsStore" : "What if I can't access the Microsoft Store?", "download.cannotAccessWindowsStore" : "What if I can't access the Microsoft Store?",
"download.macMoveToApplications" : "Open the .dmg file. Move Scratch Desktop into Applications.", "download.macMoveToApplications" : "Open the .dmg file. Move Scratch Desktop into Applications.",
"download.winMoveToApplications" : "Run the .exe file.", "download.winMoveToApplications" : "Run the .exe file.",
"download.doIHaveToDownload" : "Do I have to download an app to use Scratch?",
"download.doIHaveToDownloadAnswer" : "No. You can also use the Scratch project editor in any web browser on any device by going to scratch.mit.edu and clicking \"Create\".",
"download.canIUseScratchLink" : "Can I use Scratch Link to connect to extensions?", "download.canIUseScratchLink" : "Can I use Scratch Link to connect to extensions?",
"download.canIUseScratchLinkAnswer" : "Yes. However, you will need an Internet connection to use Scratch Link.", "download.canIUseScratchLinkAnswer" : "Yes. However, you will need an Internet connection to use Scratch Link.",
"download.canIUseExtensions" : "Can I connect to hardware extensions?", "download.canIUseExtensions" : "Can I connect to hardware extensions?",
"download.canIUseExtensionsAnswer" : "Yes. With the Scratch app you can connect to extensions, and you do not need Scratch Link.", "download.canIUseExtensionsAnswer" : "Yes. With the Scratch app you can connect to extensions, and you do not need Scratch Link.",
"download.howConnectHardwareDevices" : "How do I connect the Scratch app to hardware devices?",
"download.howConnectHardwareDevicesAnswerLink" : "You will need to install and run Scratch Link in order to connect to hardware devices when using Scratch app for {operatingsystem}. You will also need an internet connection to use Scratch Link.",
"download.howConnectHardwareDevicesAnswerApp" : "With the Scratch app you can connect to hardware devices like the micro:bit or LEGO Boost. When using the Scratch app for {operatingsystem} you do not need Scratch Link.",
"download.desktopAndBrowser": "Can I use Scratch Desktop and also have Scratch open in the browser?", "download.desktopAndBrowser": "Can I use Scratch Desktop and also have Scratch open in the browser?",
"download.appAndBrowser": "Can I use the Scratch app and also have Scratch open in the browser?", "download.appAndBrowser": "Can I use the Scratch app and also have Scratch open in the browser?",
"download.yesAnswer" : "Yes.", "download.yesAnswer" : "Yes.",
"download.onPhone": "Can I install Scratch on my Android phone?",
"download.onPhoneAnswer" : "No. The current version of Scratch for Android only works on tablets.",
"download.canIShare": "Can I share from Scratch Desktop?", "download.canIShare": "Can I share from Scratch Desktop?",
"download.canIShareAnswer": "This isnt supported currently. For now, you can save a project from Scratch Desktop, upload it to your Scratch account, and share it there. In a later version we will add the ability to upload to your Scratch account directly in Scratch Desktop.", "download.canIShareAnswer": "This isnt supported currently. For now, you can save a project from Scratch Desktop, upload it to your Scratch account, and share it there. In a later version we will add the ability to upload to your Scratch account directly in Scratch Desktop.",
"download.canIShareApp": "Can I share from Scratch for {operatingsystem}?", "download.canIShareApp": "Can I share to the online community from the Scratch app for {operatingsystem}?",
"download.canIShareAnswerPlayStore": "Yes. Click the 3-dots menu on a project in the lobby and select Share from the options. In addition to sharing by email you can sign in to your Scratch account and share the project on the Scratch Community.", "download.canIShareAnswerPlayStore": "Yes. Click the 3-dots menu on a project in the lobby and select \"Share\" from the options. In addition to sharing by email, you can sign in to your Scratch account and share a project to the Scratch online community.",
"download.canIShareAnswerDownloaded": "This isnt supported currently. For now, you can save a project from Scratch for {operatingsystem}, upload it to your Scratch account, and share it there. In a later version we will add the ability to upload to your Scratch account directly in Scratch for {operatingsystem}." "download.canIShareAnswerDownloaded": "Sharing directly to online community from the Scratch app for {operatingsystem} is not currently supported. For now, you can export a project from the Scratch app, then log onto the Scratch website, and upload and share your project there."
} }

View file

@ -59,7 +59,7 @@ const Faq = injectIntl(props => (
<li><FormattedMessage <li><FormattedMessage
id="faq.requirementsNoteDesktop" id="faq.requirementsNoteDesktop"
values={{downloadLink: ( values={{downloadLink: (
<a href="/download"><FormattedMessage id="faq.scratchDesktop" /></a> <a href="/download"><FormattedMessage id="faq.scratchApp" /></a>
)}} )}}
/></li> /></li>
<li><FormattedMessage id="faq.requirementsNoteWebGL" /></li> <li><FormattedMessage id="faq.requirementsNoteWebGL" /></li>
@ -69,7 +69,7 @@ const Faq = injectIntl(props => (
<dd><FormattedMessage <dd><FormattedMessage
id="faq.offlineBody" id="faq.offlineBody"
values={{downloadLink: ( values={{downloadLink: (
<a href="/download"><FormattedMessage id="faq.scratchDesktop" /></a> <a href="/download"><FormattedMessage id="faq.scratchApp" /></a>
)}} )}}
/></dd> /></dd>
@ -482,7 +482,7 @@ const Faq = injectIntl(props => (
id="faq.noInternetBody" id="faq.noInternetBody"
values={{downloadLink: ( values={{downloadLink: (
<a href="/download"> <a href="/download">
<FormattedMessage id="faq.scratchDesktop" /> <FormattedMessage id="faq.scratchApp" />
</a> </a>
)}} )}}
/></dd> /></dd>
@ -523,7 +523,7 @@ const Faq = injectIntl(props => (
id="faq.lawComplianceBody2" id="faq.lawComplianceBody2"
values={{downloadLink: ( values={{downloadLink: (
<a href="/download"> <a href="/download">
<FormattedMessage id="faq.scratchDesktop" /> <FormattedMessage id="faq.scratchApp" />
</a> </a>
)}} )}}
/> />

View file

@ -30,11 +30,11 @@
"faq.requirementsTabletSafari":"Mobile Safari (11+)", "faq.requirementsTabletSafari":"Mobile Safari (11+)",
"faq.requirementsNote":"Note:", "faq.requirementsNote":"Note:",
"faq.requirementsNoteDesktop":"If your computer doesnt meet these requirements, you can try the {downloadLink} editor (see next item in FAQ). ", "faq.requirementsNoteDesktop":"If your computer doesnt meet these requirements, you can try the {downloadLink} editor (see next item in FAQ). ",
"faq.scratchDesktop":"Scratch Desktop", "faq.scratchApp":"Scratch app",
"faq.requirementsNoteWebGL":"If you encounter a WebGL error, try a different browser.", "faq.requirementsNoteWebGL":"If you encounter a WebGL error, try a different browser.",
"faq.requirementsNoteTablets":"On tablets, there is currently not a way to use \"key pressed\" blocks or right-click context menus.", "faq.requirementsNoteTablets":"On tablets, there is currently not a way to use \"key pressed\" blocks or right-click context menus.",
"faq.offlineTitle":"Do you have a downloadable version so I can create and view projects offline?", "faq.offlineTitle":"Do you have a downloadable version so I can create and view projects offline?",
"faq.offlineBody":"The Scratch Desktop editor allows you to create Scratch projects without an internet connection. You can download {downloadLink} from the website. This was previously called the Scratch Offline editor.", "faq.offlineBody":"The Scratch app allows you to create Scratch projects without an internet connection. You can download the {downloadLink} from the Scratch website or the app store for your device. (This was previously called the \"Scratch Offline Editor\").",
"faq.uploadOldTitle":"Can I still upload projects created with older versions of Scratch to the website?", "faq.uploadOldTitle":"Can I still upload projects created with older versions of Scratch to the website?",
"faq.uploadOldBody":"Yes: You can share or upload projects made with earlier versions of Scratch, and they will be visible and playable. (However, you cant download projects made with or edited in later versions of Scratch and open them in earlier versions. For example, you cant open a Scratch 3.0 project in the desktop version of {scratch2Link}, because Scratch 2.0 doesnt know how to read the .sb3 project file format.)", "faq.uploadOldBody":"Yes: You can share or upload projects made with earlier versions of Scratch, and they will be visible and playable. (However, you cant download projects made with or edited in later versions of Scratch and open them in earlier versions. For example, you cant open a Scratch 3.0 project in the desktop version of {scratch2Link}, because Scratch 2.0 doesnt know how to read the .sb3 project file format.)",
"faq.scratch2":"Scratch 2.0", "faq.scratch2":"Scratch 2.0",
@ -182,9 +182,9 @@
"faq.howBody":"Scratch is used in hundreds of thousands of schools around the world, in many different subject areas (including language arts, science, history, math, and computer science). You can learn more about strategies and resources for using Scratch in schools and other learning environments (such as museums, libraries, and community centers) on our {educatorsLink}.", "faq.howBody":"Scratch is used in hundreds of thousands of schools around the world, in many different subject areas (including language arts, science, history, math, and computer science). You can learn more about strategies and resources for using Scratch in schools and other learning environments (such as museums, libraries, and community centers) on our {educatorsLink}.",
"faq.educatorsLinkText":"Educators Page", "faq.educatorsLinkText":"Educators Page",
"faq.noInternetTitle":"Is there a way for students to use Scratch without an internet connection?", "faq.noInternetTitle":"Is there a way for students to use Scratch without an internet connection?",
"faq.noInternetBody":"Yes. {downloadLink} is a version of Scratch that runs on a desktop or laptop computer. Currently, Scratch Desktop is available for Mac and Windows machines.", "faq.noInternetBody":"Yes. The {downloadLink} is a downloadable version of Scratch that can run on laptops and desktops. Currently, the Scratch app is available on Windows and Mac devices.",
"faq.communityTitle":"Can I turn off the online community for my students?", "faq.communityTitle":"Can I turn off the online community for my students?",
"faq.communityBody":"The Scratch online community provides a way for young people to share, collaborate, and learn with their peers within a moderated community governed by the Scratch {cgLink}. However, we understand that some educators prefer that their students not participate in an online community. These educators may wish to install Scratch Desktop, which runs offline and locally on a desktop or laptop computer.", "faq.communityBody":"The Scratch online community provides a way for young people to share, collaborate, and learn with their peers within a moderated community governed by the Scratch {cgLink}. However, we understand that some educators prefer that their students not participate in an online community. These educators may wish to install the Scratch app, which runs offline and locally on a desktop or laptop computer.",
"faq.teacherAccountTitle":"What is a Scratch Teacher Account?", "faq.teacherAccountTitle":"What is a Scratch Teacher Account?",
"faq.teacherAccountBody":"A Scratch Teacher Account provides teachers and other educators with additional features to manage student participation on Scratch, including the ability to create student accounts, organize student projects into studios, and monitor student comments. For more information on Scratch Teacher Accounts, see the {eduFaqLink}.", "faq.teacherAccountBody":"A Scratch Teacher Account provides teachers and other educators with additional features to manage student participation on Scratch, including the ability to create student accounts, organize student projects into studios, and monitor student comments. For more information on Scratch Teacher Accounts, see the {eduFaqLink}.",
"faq.eduFaqLinkText":"Scratch Teacher Account FAQ", "faq.eduFaqLinkText":"Scratch Teacher Account FAQ",
@ -192,7 +192,7 @@
"faq.requestBody":"You may request a Scratch Teacher Account from the {educatorsLink} on Scratch. We ask for additional information during the registration process in order to verify your role as an educator.", "faq.requestBody":"You may request a Scratch Teacher Account from the {educatorsLink} on Scratch. We ask for additional information during the registration process in order to verify your role as an educator.",
"faq.dataTitle":"What data does Scratch collect about students?", "faq.dataTitle":"What data does Scratch collect about students?",
"faq.dataBody":"When a student first signs up on Scratch, we ask for basic demographic data including gender, age (birth month and year), country, and an email address for verification. This data is used (in aggregated form) in research studies intended to improve our understanding of how people learn with Scratch. When an educator uses a Scratch Teacher Account to create student accounts in bulk, students are not required to provide an email address for account setup.", "faq.dataBody":"When a student first signs up on Scratch, we ask for basic demographic data including gender, age (birth month and year), country, and an email address for verification. This data is used (in aggregated form) in research studies intended to improve our understanding of how people learn with Scratch. When an educator uses a Scratch Teacher Account to create student accounts in bulk, students are not required to provide an email address for account setup.",
"faq.lawComplianceTitle":"Is Scratch (online version) compliant with United States local and federal data privacy laws?", "faq.lawComplianceTitle":"Is the online version of Scratch compliant with United States local and federal data privacy laws?",
"faq.lawComplianceBody1":"Scratch cares deeply about the privacy of students and of all individuals who use our platform. We have in place physical and electronic procedures to protect the information we collect on the Scratch website. Although we are not in a position to offer contractual guarantees with each entity that uses our free educational product, we are in compliance with all United States federal laws that are applicable to MIT and the Scratch Foundation, the organizations that have created and maintained Scratch. We encourage you to read the Scratch Privacy Policy for more information.", "faq.lawComplianceBody1":"Scratch cares deeply about the privacy of students and of all individuals who use our platform. We have in place physical and electronic procedures to protect the information we collect on the Scratch website. Although we are not in a position to offer contractual guarantees with each entity that uses our free educational product, we are in compliance with all United States federal laws that are applicable to MIT and the Scratch Foundation, the organizations that have created and maintained Scratch. We encourage you to read the Scratch Privacy Policy for more information.",
"faq.lawComplianceBody2":"If you would like to build projects with Scratch without submitting any Personal Information to us, you can download {downloadLink}. Projects created in Scratch Desktop are not accessible by the Scratch Team, and using Scratch Desktop does not disclose any personally identifying information to Scratch unless you upload these projects to the Scratch online community." "faq.lawComplianceBody2":"If you would like to build projects with Scratch without submitting any Personal Information to us, you can download the {downloadLink}. Projects created in the Scratch app are not accessible by the Scratch Team, and using the Scratch app does not disclose any personally identifying information to Scratch unless you upload these projects to the Scratch online community."
} }

View file

@ -27,8 +27,8 @@
"ideas.tryTheTutorial": "Try the tutorial", "ideas.tryTheTutorial": "Try the tutorial",
"ideas.codingCards": "Coding Cards", "ideas.codingCards": "Coding Cards",
"ideas.educatorGuide": "Educator Guide", "ideas.educatorGuide": "Educator Guide",
"ideas.desktopEditorHeader": "Scratch Desktop Download", "ideas.desktopEditorHeader": "Scratch App Download",
"ideas.desktopEditorBody": "To create projects without an Internet connection, you can <a href=\"/download\">download Scratch Desktop</a>.", "ideas.desktopEditorBody": "To create projects without an Internet connection, you can <a href=\"/download\">download the Scratch app</a>.",
"ideas.questionsHeader": "Questions", "ideas.questionsHeader": "Questions",
"ideas.questionsBody": "Have more questions? See the <a href=\"/info/faq\">Frequently Asked Questions</a> or visit the <a href=\"/discuss/7/\">Help with Scripts Forum</a>.", "ideas.questionsBody": "Have more questions? See the <a href=\"/info/faq\">Frequently Asked Questions</a> or visit the <a href=\"/discuss/7/\">Help with Scripts Forum</a>.",

View file

@ -8,21 +8,11 @@
left: calc(25% - 76px); left: calc(25% - 76px);
.logo { .logo {
width: 76px; width: 76px;
} }
} }
@media #{$small} { @media #{$small}, #{$medium}, #{$intermediate} {
.join {
left: calc(50% - 38px);
}
}
@media #{$medium} {
.join {
left: calc(50% - 38px);
}
}
@media #{$intermediate} {
.join { .join {
left: calc(50% - 38px); left: calc(50% - 38px);
} }

View file

@ -34,8 +34,8 @@
"parents.faqPrivacyPolicyBody": "To protect children's online privacy, we limit what we collect during the signup process, and what we make public on the website. We don't sell or rent account information to anyone. You can find out more about our {privacyPolicy} page.", "parents.faqPrivacyPolicyBody": "To protect children's online privacy, we limit what we collect during the signup process, and what we make public on the website. We don't sell or rent account information to anyone. You can find out more about our {privacyPolicy} page.",
"parents.faqFAQLinkText": "frequently asked questions page", "parents.faqFAQLinkText": "frequently asked questions page",
"parents.faqOfflineTitle": "Is there a way to use Scratch without participating online?", "parents.faqOfflineTitle": "Is there a way to use Scratch without participating online?",
"parents.faqOfflineBody" : "The Scratch Desktop editor allows you to create Scratch projects without an internet connection. You can download {scratchDesktop} from the website. This was previously called the Scratch Offline editor.", "parents.faqOfflineBody" : "Yes, the Scratch app allows you to create Scratch projects without an internet connection. You can download the {scratchApp} from the Scratch website or the app store on your device.",
"parents.faqScratchDesktop": "Scratch Desktop", "parents.faqScratchApp": "Scratch app",
"parents.faqOffline2LinkText": "Scratch 2.0 offline editor", "parents.faqOffline2LinkText": "Scratch 2.0 offline editor",
"parents.faqOffline14LinkText": "Scratch 1.4 offline editor" "parents.faqOffline14LinkText": "Scratch 1.4 offline editor"
} }

View file

@ -247,10 +247,10 @@ const Landing = () => (
<FormattedMessage <FormattedMessage
id="parents.faqOfflineBody" id="parents.faqOfflineBody"
values={{ values={{
scratchDesktop: ( scratchApp: (
<a href="/download"> <a href="/download">
<FormattedMessage <FormattedMessage
id="parents.faqScratchDesktop" id="parents.faqScratchApp"
/> />
</a> </a>
) )

View file

@ -0,0 +1,97 @@
// embed view
const React = require('react');
const PropTypes = require('prop-types');
const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl;
const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const NotAvailable = require('../../components/not-available/not-available.jsx');
const Meta = require('./meta.jsx');
const previewActions = require('../../redux/preview.js');
const GUI = require('scratch-gui');
const IntlGUI = injectIntl(GUI.default);
const initSentry = require('../../lib/sentry.js');
initSentry();
class EmbedView extends React.Component {
constructor (props) {
super(props);
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
this.state = {
extensions: [],
invalidProject: parts.length === 1,
projectId: parts[1]
};
}
componentDidMount () {
this.props.getProjectInfo(this.state.projectId);
}
render () {
if (this.props.projectNotAvailable || this.state.invalidProject) {
return (
<ErrorBoundary>
<div className="preview">
<NotAvailable />
</div>
</ErrorBoundary>
);
}
return (
<React.Fragment>
<Meta projectInfo={this.props.projectInfo} />
<IntlGUI
assetHost={this.props.assetHost}
basePath="/"
className="gui"
projectHost={this.props.projectHost}
projectId={this.state.projectId}
projectTitle={this.props.projectInfo.title}
/>
</React.Fragment>
);
}
}
EmbedView.propTypes = {
assetHost: PropTypes.string.isRequired,
getProjectInfo: PropTypes.func.isRequired,
projectHost: PropTypes.string.isRequired,
projectInfo: projectShape,
projectNotAvailable: PropTypes.bool
};
EmbedView.defaultProps = {
assetHost: process.env.ASSET_HOST,
projectHost: process.env.PROJECT_HOST
};
const mapStateToProps = state => ({
projectInfo: state.preview.projectInfo,
projectNotAvailable: state.preview.projectNotAvailable
});
const mapDispatchToProps = dispatch => ({
getProjectInfo: (id, token) => {
dispatch(previewActions.getProjectInfo(id, token));
}
});
module.exports.View = connect(
mapStateToProps,
mapDispatchToProps
)(EmbedView);
GUI.setAppElement(document.getElementById('app'));
module.exports.initGuiState = GUI.initEmbedded;
module.exports.guiReducers = GUI.guiReducers;
module.exports.guiInitialState = GUI.guiInitialState;
module.exports.guiMiddleware = GUI.guiMiddleware;
module.exports.initLocale = GUI.initLocale;
module.exports.localesInitialState = GUI.localesInitialState;

View file

@ -0,0 +1,30 @@
const React = require('react');
const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx');
const render = require('../../lib/render.jsx');
// Require this even though we don't use it because, without it, webpack runs out of memory...
const Page = require('../../components/page/www/page.jsx'); // eslint-disable-line no-unused-vars
const previewActions = require('../../redux/preview.js');
const isSupportedBrowser = require('../../lib/supported-browser').default;
const UnsupportedBrowser = require('./unsupported-browser.jsx');
if (isSupportedBrowser()) {
const EmbedView = require('./embed-view.jsx');
render(
<EmbedView.View />,
document.getElementById('app'),
{
preview: previewActions.previewReducer,
...EmbedView.guiReducers
},
{
locales: EmbedView.initLocale(EmbedView.localesInitialState, window._locale),
scratchGui: EmbedView.initGuiState(EmbedView.guiInitialState)
},
EmbedView.guiMiddleware
);
} else {
render(<ErrorBoundary><UnsupportedBrowser /></ErrorBoundary>, document.getElementById('app'));
}

View file

@ -35,17 +35,8 @@ const IntlGUI = injectIntl(GUI.default);
const localStorageAvailable = 'localStorage' in window && window.localStorage !== null; const localStorageAvailable = 'localStorage' in window && window.localStorage !== null;
const Sentry = require('@sentry/browser'); const initSentry = require('../../lib/sentry.js');
if (`${process.env.SENTRY_DSN}` !== '') { initSentry();
Sentry.init({
dsn: `${process.env.SENTRY_DSN}`,
// Do not collect global onerror, only collect specifically from React error boundaries.
// TryCatch plugin also includes errors from setTimeouts (i.e. the VM)
integrations: integrations => integrations.filter(i =>
!(i.name === 'GlobalHandlers' || i.name === 'TryCatch'))
});
window.Sentry = Sentry; // Allow GUI access to Sentry via window
}
class Preview extends React.Component { class Preview extends React.Component {
constructor (props) { constructor (props) {
@ -258,7 +249,7 @@ class Preview extends React.Component {
// this is a project.json as an object // this is a project.json as an object
// validate expects a string or buffer as input // validate expects a string or buffer as input
// TODO not sure if we need to check that it also isn't a data view // TODO not sure if we need to check that it also isn't a data view
input = JSON.stringify(input); input = JSON.stringify(input); // NOTE: what is the point of doing this??
} }
parser(projectAsset.data, false, (err, projectData) => { parser(projectAsset.data, false, (err, projectData) => {
if (err) { if (err) {
@ -1092,16 +1083,13 @@ module.exports.initGuiState = guiInitialState => {
const parts = pathname.split('/').filter(Boolean); const parts = pathname.split('/').filter(Boolean);
// parts[0]: 'projects' // parts[0]: 'projects'
// parts[1]: either :id or 'editor' // parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor', 'fullscreen' or 'embed' // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
if (parts.indexOf('editor') === -1) { if (parts.indexOf('editor') === -1) {
guiInitialState = GUI.initPlayer(guiInitialState); guiInitialState = GUI.initPlayer(guiInitialState);
} }
if (parts.indexOf('fullscreen') !== -1) { if (parts.indexOf('fullscreen') !== -1) {
guiInitialState = GUI.initFullScreen(guiInitialState); guiInitialState = GUI.initFullScreen(guiInitialState);
} }
if (parts.indexOf('embed') !== -1) {
guiInitialState = GUI.initEmbedded(guiInitialState);
}
return guiInitialState; return guiInitialState;
}; };

View file

@ -0,0 +1,70 @@
const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
import configureStore from 'redux-mock-store';
const Navigation = require('../../../src/components/navigation/www/navigation.jsx');
const sessionActions = require('../../../src/redux/session.js');
describe('Navigation', () => {
const mockStore = configureStore();
let store;
beforeEach(() => {
store = null;
});
const getNavigationWrapper = props => {
const wrapper = shallowWithIntl(
<Navigation
{...props}
/>
, {context: {store}}
);
return wrapper
.dive() // unwrap redux connect(injectIntl(JoinFlow))
.dive(); // unwrap injectIntl(JoinFlow)
};
test('when using old join flow, clicking Join Scratch attemps to open registration', () => {
store = mockStore({
navigation: {
useScratch3Registration: false
},
session: {
status: sessionActions.Status.FETCHED
},
messageCount: {
messageCount: 0
}
});
const props = {
handleOpenRegistration: jest.fn()
};
const navWrapper = getNavigationWrapper(props);
const navInstance = navWrapper.instance();
navWrapper.find('a.registrationLink').simulate('click');
expect(navInstance.props.handleOpenRegistration).toHaveBeenCalled();
});
test('when using new join flow, clicking Join Scratch attemps to navigate to registration', () => {
store = mockStore({
navigation: {
useScratch3Registration: true
},
session: {
status: sessionActions.Status.FETCHED
},
messageCount: {
messageCount: 0
}
});
const props = {
navigateToRegistration: jest.fn()
};
const navWrapper = getNavigationWrapper(props);
const navInstance = navWrapper.instance();
navWrapper.find('a.registrationLink').simulate('click');
expect(navInstance.props.navigateToRegistration).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,160 @@
describe('fastly library', () => {
let mockedFastlyRequest = {};
jest.mock('fastly', () => (() => ({
request: mockedFastlyRequest
})));
const fastlyExtended = require('../../../bin/lib/fastly-extended'); // eslint-disable-line global-require
test('getLatestActiveVersion returns largest active VCL number, ' +
'when called with VCLs in sequential order', done => {
mockedFastlyRequest = jest.fn((method, url, cb) => {
cb(null, [
{
number: 1,
active: false
},
{
number: 2,
active: false
},
{
number: 3,
active: true
},
{
number: 4,
active: false
}
]);
});
const fastlyInstance = fastlyExtended('api_key', 'service_id');
fastlyInstance.getLatestActiveVersion((err, response) => {
expect(err).toBe(null);
expect(response).toEqual({
number: 3,
active: true
});
expect(mockedFastlyRequest).toHaveBeenCalledWith(
'GET', '/service/service_id/version', expect.any(Function)
);
done();
});
});
test('getLatestActiveVersion returns largest active VCL number, when called with VCLs in mixed up order', done => {
mockedFastlyRequest = jest.fn((method, url, cb) => {
cb(null, [
{
number: 4,
active: false
},
{
number: 1,
active: false
},
{
number: 2,
active: true
},
{
number: 3,
active: false
}
]);
});
const fastlyInstance = fastlyExtended('api_key', 'service_id');
fastlyInstance.getLatestActiveVersion((err, response) => {
expect(err).toBe(null);
expect(response).toEqual({
number: 2,
active: true
});
expect(mockedFastlyRequest).toHaveBeenCalledWith(
'GET', '/service/service_id/version', expect.any(Function)
);
done();
});
});
test('getLatestActiveVersion returns null, when none of the VCL versions are active', done => {
mockedFastlyRequest = jest.fn((method, url, cb) => {
cb(null, [
{
number: 4,
active: false
},
{
number: 1,
active: false
},
{
number: 2,
active: false
},
{
number: 3,
active: false
}
]);
});
const fastlyInstance = fastlyExtended('api_key', 'service_id');
fastlyInstance.getLatestActiveVersion((err, response) => {
expect(err).toBe(null);
expect(response).toEqual(null);
expect(mockedFastlyRequest).toHaveBeenCalledWith(
'GET', '/service/service_id/version', expect.any(Function)
);
done();
});
});
test('getLatestActiveVersion returns largest active VCL number, ' +
'when called with a single active VCL', done => {
mockedFastlyRequest = jest.fn((method, url, cb) => {
cb(null, [
{
number: 1,
active: true
}
]);
});
const fastlyInstance = fastlyExtended('api_key', 'service_id');
fastlyInstance.getLatestActiveVersion((err, response) => {
expect(err).toBe(null);
expect(response).toEqual({
number: 1,
active: true
});
expect(mockedFastlyRequest).toHaveBeenCalledWith(
'GET', '/service/service_id/version', expect.any(Function)
);
done();
});
});
test('getLatestActiveVersion returns null, when called with a single inactive VCL', done => {
mockedFastlyRequest = jest.fn((method, url, cb) => {
cb(null, [
{
number: 1,
active: false
}
]);
});
const fastlyInstance = fastlyExtended('api_key', 'service_id');
fastlyInstance.getLatestActiveVersion((err, response) => {
expect(err).toBe(null);
expect(response).toEqual(null);
expect(mockedFastlyRequest).toHaveBeenCalledWith(
'GET', '/service/service_id/version', expect.any(Function)
);
done();
});
});
});