From 775173661fade65b7f8ef0d65a79d6514ecc1de5 Mon Sep 17 00:00:00 2001
From: Ben Wheeler <wheeler.benjamin@gmail.com>
Date: Tue, 17 Sep 2019 21:49:48 -0400
Subject: [PATCH] embed view with minimal functionality, route

---
 src/routes.json                    |  10 +-
 src/views/preview/embed-view.jsx   | 154 +++++++++++++++++++++++++++++
 src/views/preview/embed.jsx        |  29 ++++++
 src/views/preview/project-view.jsx |   5 +-
 4 files changed, 193 insertions(+), 5 deletions(-)
 create mode 100644 src/views/preview/embed-view.jsx
 create mode 100644 src/views/preview/embed.jsx

diff --git a/src/routes.json b/src/routes.json
index c1494768f..4cfeb3963 100644
--- a/src/routes.json
+++ b/src/routes.json
@@ -177,12 +177,20 @@
     },
     {
         "name": "projects",
-        "pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen|/embed)?)?)?/?(\\?.*)?$",
+        "pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen)?)?)?/?(\\?.*)?$",
         "routeAlias": "/projects/?$",
         "view": "preview/preview",
         "title": "Scratch Project",
         "dynamicMetaTags": true
     },
+    {
+        "name": "projects",
+        "pattern": "^/projects/\\d+/embed/?(\\?.*)?$",
+        "routeAlias": "/projects/?$",
+        "view": "preview/embed",
+        "title": "Scratch Project",
+        "dynamicMetaTags": true
+    },
     {
         "name": "parents",
         "pattern": "^/parents/?(\\?.*)?$",
diff --git a/src/views/preview/embed-view.jsx b/src/views/preview/embed-view.jsx
new file mode 100644
index 000000000..94d6afe5c
--- /dev/null
+++ b/src/views/preview/embed-view.jsx
@@ -0,0 +1,154 @@
+// embed view
+
+const bindAll = require('lodash.bindall');
+const React = require('react');
+const PropTypes = require('prop-types');
+const connect = require('react-redux').connect;
+const injectIntl = require('react-intl').injectIntl;
+
+const Page = require('../../components/page/www/page.jsx');
+const storage = require('../../lib/storage.js').default;
+const jar = require('../../lib/jar.js');
+const projectShape = require('./projectshape.jsx').projectShape;
+const NotAvailable = require('../../components/not-available/not-available.jsx');
+const Meta = require('./meta.jsx');
+
+const sessionActions = require('../../redux/session.js');
+const previewActions = require('../../redux/preview.js');
+
+const GUI = require('scratch-gui');
+const IntlGUI = injectIntl(GUI.default);
+
+const Sentry = require('@sentry/browser');
+if (`${process.env.SENTRY_DSN}` !== '') {
+    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 EmbedView extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+        ]);
+        const pathname = window.location.pathname.toLowerCase();
+        const parts = pathname.split('/').filter(Boolean);
+        this.state = {
+            extensions: [],
+            invalidProject: parts.length === 1,
+            projectId: parts[1]
+        };
+    }
+    componentDidUpdate (prevProps) {
+        if (this.state.projectId > 0 &&
+            ((this.props.sessionStatus !== prevProps.sessionStatus &&
+            this.props.sessionStatus === sessionActions.Status.FETCHED))) {
+            this.props.getProjectInfo(this.state.projectId);
+            this.getProjectData(this.state.projectId, true /* Show cloud/username alerts */);
+        }
+    }
+    getProjectData (projectId) {
+        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);
+                }
+            });
+    }
+    handleSetLanguage (locale) {
+        jar.set('scratchlanguage', locale);
+    }
+    render () {
+        if (this.props.projectNotAvailable || this.state.invalidProject) {
+            return (
+                <Page>
+                    <div className="preview">
+                        <NotAvailable />
+                    </div>
+                </Page>
+            );
+        }
+
+        return (
+            <React.Fragment>
+                <Meta projectInfo={this.props.projectInfo} />
+                <React.Fragment>
+                    <IntlGUI
+                        assetHost={this.props.assetHost}
+                        basePath="/"
+                        className="gui"
+                        projectHost={this.props.projectHost}
+                        projectId={this.state.projectId}
+                        projectTitle={this.props.projectInfo.title}
+                        onSetLanguage={this.handleSetLanguage}
+                    />
+                </React.Fragment>
+            </React.Fragment>
+        );
+    }
+}
+
+EmbedView.propTypes = {
+    assetHost: PropTypes.string.isRequired,
+    getProjectInfo: PropTypes.func.isRequired,
+    projectHost: PropTypes.string.isRequired,
+    projectInfo: projectShape,
+    projectNotAvailable: PropTypes.bool,
+    sessionStatus: PropTypes.string
+};
+
+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);
+
+// initialize GUI by calling its reducer functions depending on URL
+GUI.setAppElement(document.getElementById('app'));
+module.exports.initGuiState = guiInitialState => {
+    const pathname = window.location.pathname.toLowerCase();
+    const parts = pathname.split('/').filter(Boolean);
+    // parts[0]: 'projects'
+    // parts[1]: either :id or 'editor'
+    // parts[2]: undefined if no :id, otherwise either 'editor', 'fullscreen' or 'embed'
+    if (parts.indexOf('embed') !== -1) {
+        guiInitialState = GUI.initEmbedded(guiInitialState);
+    }
+    return guiInitialState;
+};
+
+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;
diff --git a/src/views/preview/embed.jsx b/src/views/preview/embed.jsx
new file mode 100644
index 000000000..278fe3591
--- /dev/null
+++ b/src/views/preview/embed.jsx
@@ -0,0 +1,29 @@
+// preview view can show either project page or editor page;
+// idea is that we shouldn't require a page reload to switch back and forth
+const React = require('react');
+const Page = require('../../components/page/www/page.jsx');
+const render = require('../../lib/render.jsx');
+
+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(<Page><UnsupportedBrowser /></Page>, document.getElementById('app'));
+}
diff --git a/src/views/preview/project-view.jsx b/src/views/preview/project-view.jsx
index f31353bc1..97ffd7fd3 100644
--- a/src/views/preview/project-view.jsx
+++ b/src/views/preview/project-view.jsx
@@ -1092,16 +1092,13 @@ module.exports.initGuiState = guiInitialState => {
     const parts = pathname.split('/').filter(Boolean);
     // parts[0]: 'projects'
     // 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) {
         guiInitialState = GUI.initPlayer(guiInitialState);
     }
     if (parts.indexOf('fullscreen') !== -1) {
         guiInitialState = GUI.initFullScreen(guiInitialState);
     }
-    if (parts.indexOf('embed') !== -1) {
-        guiInitialState = GUI.initEmbedded(guiInitialState);
-    }
     return guiInitialState;
 };