From e2e1ee77445e92769659929f9433abae2cc20688 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 17 Dec 2018 14:23:58 -0800 Subject: [PATCH 1/5] Add isScratchDesktop func, use it in playground --- src/lib/isScratchDesktop.js | 22 ++++++++++++++++++++++ src/playground/render-gui.jsx | 23 +++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/lib/isScratchDesktop.js diff --git a/src/lib/isScratchDesktop.js b/src/lib/isScratchDesktop.js new file mode 100644 index 000000000..c903041fb --- /dev/null +++ b/src/lib/isScratchDesktop.js @@ -0,0 +1,22 @@ +/** + * @returns {boolean} - true if is seems like the GUI is running under Scratch Desktop; false otherwise. + * This affects various things throughout the GUI. Ideally we should use some other way to check for this, but it + * should still be possible to override it externally for testing purposes. For example, maybe this should be a + * property on the GUI element or part of the global GUI state. Calls to this function should be taken as a + * placeholder for a future, better approach. + */ +const isScratchDesktop = function () { + if (isScratchDesktop.override !== null) { + return isScratchDesktop.override; + } + const userAgent = navigator.userAgent.toLowerCase(); + const isElectron = (userAgent.indexOf(' electron/') > -1); + return isElectron; +}; + +/** + * Set this value to non-null to force `isScratchDesktop` to return that value for testing & dev purposes. + */ +isScratchDesktop.override = null; + +export default isScratchDesktop; diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index f3acac2ee..ae3ffa4c3 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -5,6 +5,7 @@ import {compose} from 'redux'; import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; +import isScratchDesktop from '../lib/isScratchDesktop'; import TitledHOC from '../lib/titled-hoc.jsx'; const onClickLogo = () => { @@ -28,6 +29,20 @@ export default appTarget => { TitledHOC )(GUI); + // hack for testing the GUI in Scratch Desktop mode + // add "?isScratchDesktop=1" or similar to the URL + const scratchDesktopMatches = window.location.href.match(/[?&]isScratchDesktop=([^&]+)/); + if (scratchDesktopMatches) { + try { + // parse 'true' into `true`, 'false' into `false`, etc. + isScratchDesktop.override = JSON.parse(scratchDesktopMatches[1]); + } catch { + // it's not JSON so just use the string + // note that a typo like "falsy" will be treated as true + isScratchDesktop.override = scratchDesktopMatches[1]; + } + } + // TODO a hack for testing the backpack, allow backpack host to be set by url param const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; @@ -39,12 +54,12 @@ export default appTarget => { ReactDOM.render( , appTarget); }; From aa23b4109a1e1134c65d9d427933a32ccdf3a2e1 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 17 Dec 2018 15:18:29 -0800 Subject: [PATCH 2/5] Exclude 'Animate an Adventure' from Scratch Desktop --- src/lib/isScratchDesktop.js | 21 +++++++++++++++++++++ src/lib/libraries/decks/index.jsx | 17 +++++++++++++++-- src/playground/index.jsx | 4 ++++ src/playground/render-gui.jsx | 17 ++--------------- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/lib/isScratchDesktop.js b/src/lib/isScratchDesktop.js index c903041fb..8d573e91e 100644 --- a/src/lib/isScratchDesktop.js +++ b/src/lib/isScratchDesktop.js @@ -19,4 +19,25 @@ const isScratchDesktop = function () { */ isScratchDesktop.override = null; +/** + * Parse an HREF string and extract an `isScratchDesktop` override value if present. + * The value is set in `isScratchDesktop.override`. + * Activate this by adding "?isScratchDesktop=1" or similar to the URL. + * @param {string} href - the `window.location.href` value (or equivalent) to parse + */ +const setIsScratchDesktopOverrideFromHref = function (href) { + const scratchDesktopMatches = href.match(/[?&]isScratchDesktop=([^&]+)/); + if (scratchDesktopMatches) { + try { + // parse 'true' into `true`, 'false' into `false`, etc. + isScratchDesktop.override = JSON.parse(scratchDesktopMatches[1]); + } catch { + // it's not JSON so just use the string + // note that a typo like "falsy" will be treated as true + isScratchDesktop.override = scratchDesktopMatches[1]; + } + } +}; + export default isScratchDesktop; +export {setIsScratchDesktopOverrideFromHref}; diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx index 78aeb6f29..b8aefa207 100644 --- a/src/lib/libraries/decks/index.jsx +++ b/src/lib/libraries/decks/index.jsx @@ -1,6 +1,8 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; +import isScratchDesktop from '../../../lib/isScratchDesktop'; + // Intro import libraryIntro from './intro/lib-getting-started.jpg'; import stepMove from './intro/intro1.gif'; @@ -116,8 +118,7 @@ import addEffectsThumb from './videos/add-effects.jpg'; import moveArrowKeysThumb from './videos/move-arrow-keys.jpg'; import spinThumb from './videos/spin.jpg'; -export default { - +const decksRaw = { 'intro-move-sayhello': { name: ( !deckInfo.hasOwnProperty('requiredProjectId')) + .reduce((newDecks, [key, deckInfo]) => { + newDecks[key] = deckInfo; + return newDecks; + }, {}) : + decksRaw; + +export default decks; diff --git a/src/playground/index.jsx b/src/playground/index.jsx index dcf6fa181..91555003a 100644 --- a/src/playground/index.jsx +++ b/src/playground/index.jsx @@ -7,6 +7,10 @@ import 'intl'; // For Safari 9 import React from 'react'; import ReactDOM from 'react-dom'; +// this should be done before our other imports in case any use `isScratchDesktop()` right away +import {setIsScratchDesktopOverrideFromHref} from '../lib/isScratchDesktop'; +setIsScratchDesktopOverrideFromHref(window.location.href); + import analytics from '../lib/analytics'; import AppStateHOC from '../lib/app-state-hoc.jsx'; import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index ae3ffa4c3..ab4931f78 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -2,10 +2,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {compose} from 'redux'; +import isScratchDesktop from '../lib/isScratchDesktop'; + import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; -import isScratchDesktop from '../lib/isScratchDesktop'; import TitledHOC from '../lib/titled-hoc.jsx'; const onClickLogo = () => { @@ -29,20 +30,6 @@ export default appTarget => { TitledHOC )(GUI); - // hack for testing the GUI in Scratch Desktop mode - // add "?isScratchDesktop=1" or similar to the URL - const scratchDesktopMatches = window.location.href.match(/[?&]isScratchDesktop=([^&]+)/); - if (scratchDesktopMatches) { - try { - // parse 'true' into `true`, 'false' into `false`, etc. - isScratchDesktop.override = JSON.parse(scratchDesktopMatches[1]); - } catch { - // it's not JSON so just use the string - // note that a typo like "falsy" will be treated as true - isScratchDesktop.override = scratchDesktopMatches[1]; - } - } - // TODO a hack for testing the backpack, allow backpack host to be set by url param const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; From b9dc953dede6be4f9f6a5d45ce6dd22dc60e641a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 19 Dec 2018 01:08:33 -0800 Subject: [PATCH 3/5] Derive isScratchDesktop state from a GUI prop --- src/containers/gui.jsx | 5 +++ src/containers/tips-library.jsx | 27 +++++++++------ src/lib/isScratchDesktop.js | 56 ++++++++++++++----------------- src/lib/libraries/decks/index.jsx | 17 ++-------- src/playground/index.jsx | 4 --- src/playground/render-gui.jsx | 37 ++++++++++++++------ 6 files changed, 76 insertions(+), 70 deletions(-) diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 9935b3a7e..025a73966 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -36,6 +36,7 @@ import vmManagerHOC from '../lib/vm-manager-hoc.jsx'; import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx'; import GUIComponent from '../components/gui/gui.jsx'; +import {setIsScratchDesktop} from '../lib/isScratchDesktop.js'; const messages = defineMessages({ defaultProjectTitle: { @@ -47,6 +48,7 @@ const messages = defineMessages({ class GUI extends React.Component { componentDidMount () { + setIsScratchDesktop(this.props.isScratchDesktop); this.setReduxTitle(this.props.projectTitle); this.props.onStorageInit(storage); } @@ -78,6 +80,7 @@ class GUI extends React.Component { cloudHost, error, isError, + isScratchDesktop, isShowingProject, onStorageInit, onUpdateProjectId, @@ -113,6 +116,7 @@ GUI.propTypes = { intl: intlShape, isError: PropTypes.bool, isLoading: PropTypes.bool, + isScratchDesktop: PropTypes.bool, isShowingProject: PropTypes.bool, loadingStateVisible: PropTypes.bool, onSeeCommunity: PropTypes.func, @@ -128,6 +132,7 @@ GUI.propTypes = { }; GUI.defaultProps = { + isScratchDesktop: false, onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(), onUpdateProjectId: () => {} }; diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx index 5727e19d1..e5cb311aa 100644 --- a/src/containers/tips-library.jsx +++ b/src/containers/tips-library.jsx @@ -7,6 +7,8 @@ import decksLibraryContent from '../lib/libraries/decks/index.jsx'; import tutorialTags from '../lib/libraries/tutorial-tags'; import analytics from '../lib/analytics'; +import {notScratchDesktop} from '../lib/isScratchDesktop'; + import LibraryComponent from '../components/library/library.jsx'; import {connect} from 'react-redux'; @@ -56,16 +58,21 @@ class TipsLibrary extends React.PureComponent { }); } render () { - const decksLibraryThumbnailData = Object.keys(decksLibraryContent).map(id => ({ - rawURL: decksLibraryContent[id].img, - id: id, - name: decksLibraryContent[id].name, - featured: true, - tags: decksLibraryContent[id].tags, - urlId: decksLibraryContent[id].urlId, - requiredProjectId: decksLibraryContent[id].requiredProjectId, - hidden: decksLibraryContent[id].hidden || false - })); + const decksLibraryThumbnailData = Object.entries(decksLibraryContent) + .filter(([, item]) => + // Scratch Desktop doesn't want tutorials with `requiredProjectId` + notScratchDesktop() || !item.hasOwnProperty('requiredProjectId') + ) + .map(([id, item]) => ({ + rawURL: item.img, + id: id, + name: item.name, + featured: true, + tags: item.tags, + urlId: item.urlId, + requiredProjectId: item.requiredProjectId, + hidden: item.hidden || false + })); if (!this.props.visible) return null; return ( diff --git a/src/lib/isScratchDesktop.js b/src/lib/isScratchDesktop.js index 8d573e91e..3f58a8842 100644 --- a/src/lib/isScratchDesktop.js +++ b/src/lib/isScratchDesktop.js @@ -1,43 +1,37 @@ /** - * @returns {boolean} - true if is seems like the GUI is running under Scratch Desktop; false otherwise. - * This affects various things throughout the GUI. Ideally we should use some other way to check for this, but it - * should still be possible to override it externally for testing purposes. For example, maybe this should be a - * property on the GUI element or part of the global GUI state. Calls to this function should be taken as a - * placeholder for a future, better approach. + * Internal stored state. Not valid until after at least one call to `setIsScratchDesktop()`. + * @type {boolean} */ -const isScratchDesktop = function () { - if (isScratchDesktop.override !== null) { - return isScratchDesktop.override; - } - const userAgent = navigator.userAgent.toLowerCase(); - const isElectron = (userAgent.indexOf(' electron/') > -1); - return isElectron; +let _isScratchDesktop; // undefined = not ready yet + +/** + * Tell the `isScratchDesktop()` whether or not the GUI is running under Scratch Desktop. + * @param {boolean} value - the new value which `isScratchDesktop()` should return in the future. + */ +const setIsScratchDesktop = function (value) { + _isScratchDesktop = value; }; /** - * Set this value to non-null to force `isScratchDesktop` to return that value for testing & dev purposes. + * @returns {boolean} - true if it seems like the GUI is running under Scratch Desktop; false otherwise. */ -isScratchDesktop.override = null; +const isScratchDesktop = function () { + if (typeof _isScratchDesktop === 'undefined') { + throw new Error('isScratchDesktop is not ready yet'); + } + return _isScratchDesktop; +}; /** - * Parse an HREF string and extract an `isScratchDesktop` override value if present. - * The value is set in `isScratchDesktop.override`. - * Activate this by adding "?isScratchDesktop=1" or similar to the URL. - * @param {string} href - the `window.location.href` value (or equivalent) to parse + * @returns {boolean} - false if it seems like the GUI is running under Scratch Desktop; true otherwise. */ -const setIsScratchDesktopOverrideFromHref = function (href) { - const scratchDesktopMatches = href.match(/[?&]isScratchDesktop=([^&]+)/); - if (scratchDesktopMatches) { - try { - // parse 'true' into `true`, 'false' into `false`, etc. - isScratchDesktop.override = JSON.parse(scratchDesktopMatches[1]); - } catch { - // it's not JSON so just use the string - // note that a typo like "falsy" will be treated as true - isScratchDesktop.override = scratchDesktopMatches[1]; - } - } +const notScratchDesktop = function () { + return !isScratchDesktop(); }; export default isScratchDesktop; -export {setIsScratchDesktopOverrideFromHref}; +export { + isScratchDesktop, + notScratchDesktop, + setIsScratchDesktop +}; diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx index b8aefa207..78aeb6f29 100644 --- a/src/lib/libraries/decks/index.jsx +++ b/src/lib/libraries/decks/index.jsx @@ -1,8 +1,6 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import isScratchDesktop from '../../../lib/isScratchDesktop'; - // Intro import libraryIntro from './intro/lib-getting-started.jpg'; import stepMove from './intro/intro1.gif'; @@ -118,7 +116,8 @@ import addEffectsThumb from './videos/add-effects.jpg'; import moveArrowKeysThumb from './videos/move-arrow-keys.jpg'; import spinThumb from './videos/spin.jpg'; -const decksRaw = { +export default { + 'intro-move-sayhello': { name: ( !deckInfo.hasOwnProperty('requiredProjectId')) - .reduce((newDecks, [key, deckInfo]) => { - newDecks[key] = deckInfo; - return newDecks; - }, {}) : - decksRaw; - -export default decks; diff --git a/src/playground/index.jsx b/src/playground/index.jsx index 91555003a..dcf6fa181 100644 --- a/src/playground/index.jsx +++ b/src/playground/index.jsx @@ -7,10 +7,6 @@ import 'intl'; // For Safari 9 import React from 'react'; import ReactDOM from 'react-dom'; -// this should be done before our other imports in case any use `isScratchDesktop()` right away -import {setIsScratchDesktopOverrideFromHref} from '../lib/isScratchDesktop'; -setIsScratchDesktopOverrideFromHref(window.location.href); - import analytics from '../lib/analytics'; import AppStateHOC from '../lib/app-state-hoc.jsx'; import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index ab4931f78..3ae35e4d3 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -2,8 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {compose} from 'redux'; -import isScratchDesktop from '../lib/isScratchDesktop'; - import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; @@ -34,19 +32,38 @@ export default appTarget => { const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; + const scratchDesktopMatches = window.location.href.match(/[?&]isScratchDesktop=([^&]+)/); + let simulateScratchDesktop; + if (scratchDesktopMatches) { + try { + // parse 'true' into `true`, 'false' into `false`, etc. + simulateScratchDesktop = JSON.parse(scratchDesktopMatches[1]); + } catch { + // it's not JSON so just use the string + // note that a typo like "falsy" will be treated as true + simulateScratchDesktop = scratchDesktopMatches[1]; + } + } + if (process.env.NODE_ENV === 'production' && typeof window === 'object') { // Warn before navigating away window.onbeforeunload = () => true; } ReactDOM.render( - , + // important: this is checking whether `simulateScratchDesktop` is truthy, not just defined! + simulateScratchDesktop ? + : + , appTarget); }; From 75053b52d1b838f67edefecb94c047d61f165720 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 19 Dec 2018 08:49:46 -0800 Subject: [PATCH 4/5] Don't use Object.keys() --- src/containers/tips-library.jsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx index e5cb311aa..3c85ce99e 100644 --- a/src/containers/tips-library.jsx +++ b/src/containers/tips-library.jsx @@ -58,20 +58,20 @@ class TipsLibrary extends React.PureComponent { }); } render () { - const decksLibraryThumbnailData = Object.entries(decksLibraryContent) - .filter(([, item]) => + const decksLibraryThumbnailData = Object.keys(decksLibraryContent) + .filter(id => // Scratch Desktop doesn't want tutorials with `requiredProjectId` - notScratchDesktop() || !item.hasOwnProperty('requiredProjectId') + notScratchDesktop() || !decksLibraryContent[id].hasOwnProperty('requiredProjectId') ) - .map(([id, item]) => ({ - rawURL: item.img, + .map(id => ({ + rawURL: decksLibraryContent[id].img, id: id, - name: item.name, + name: decksLibraryContent[id].name, featured: true, - tags: item.tags, - urlId: item.urlId, - requiredProjectId: item.requiredProjectId, - hidden: item.hidden || false + tags: decksLibraryContent[id].tags, + urlId: decksLibraryContent[id].urlId, + requiredProjectId: decksLibraryContent[id].requiredProjectId, + hidden: decksLibraryContent[id].hidden || false })); if (!this.props.visible) return null; From af50ac76bf0c0796be0ad30049c6d1ad1814256a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 19 Dec 2018 09:50:22 -0800 Subject: [PATCH 5/5] Remove ready check from isScratchDesktop() This was intended as a safety check but it causes problems with some tests. --- src/lib/isScratchDesktop.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/isScratchDesktop.js b/src/lib/isScratchDesktop.js index 3f58a8842..e159e3852 100644 --- a/src/lib/isScratchDesktop.js +++ b/src/lib/isScratchDesktop.js @@ -14,11 +14,9 @@ const setIsScratchDesktop = function (value) { /** * @returns {boolean} - true if it seems like the GUI is running under Scratch Desktop; false otherwise. + * If `setIsScratchDesktop()` has not yet been called, this can return `undefined`. */ const isScratchDesktop = function () { - if (typeof _isScratchDesktop === 'undefined') { - throw new Error('isScratchDesktop is not ready yet'); - } return _isScratchDesktop; };