From a9933242e0daa417fd9ccb2492b33f83f77d5954 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 15 Sep 2020 14:32:36 -0700
Subject: [PATCH] use project state system to load initial project

---
 src/renderer/app.jsx | 112 +++++++++++++++++++++++++++++++++----------
 1 file changed, 87 insertions(+), 25 deletions(-)

diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 9ad78c0..0e73fff 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -1,16 +1,33 @@
 import {ipcRenderer, shell} from 'electron';
 import bindAll from 'lodash.bindall';
+import omit from 'lodash.omit';
+import PropTypes from 'prop-types';
 import React from 'react';
 import ReactDOM from 'react-dom';
+import {connect} from 'react-redux';
 import {compose} from 'redux';
-import GUI, {AppStateHOC} from 'scratch-gui';
+import GUI from 'scratch-gui/src/index';
+import VM from 'scratch-vm';
+
+import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.jsx';
+import {
+    LoadingStates,
+    onFetchedProjectData,
+    onLoadedProject,
+    defaultProjectId,
+    requestNewProject,
+    requestProjectUpload,
+    setProjectId
+} from 'scratch-gui/src/reducers/project-state';
+import {
+    openLoadingProject,
+    closeLoadingProject
+} from 'scratch-gui/src/reducers/modals';
 
 import ElectronStorageHelper from '../common/ElectronStorageHelper';
 
 import styles from './app.css';
 
-const defaultProjectId = 0;
-
 // override window.open so that it uses the OS's default browser, not an electron browser
 window.open = function (url, target) {
     if (target === '_blank') {
@@ -38,16 +55,29 @@ const ScratchDesktopHOC = function (WrappedComponent) {
                 'handleTelemetryModalOptOut',
                 'handleUpdateProjectTitle'
             ]);
-            this.state = {
-                projectTitle: null,
-                projectLoading: true
-            };
+            this.props.onLoadingStarted();
+            ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => {
+                const hasInitialProject = initialProjectData && (initialProjectData.length > 0);
+                this.props.onHasInitialProject(hasInitialProject, this.props.loadingState);
+                if (!hasInitialProject) {
+                    this.props.onLoadingCompleted();
+                    return;
+                }
+                this.props.vm.loadProject(initialProjectData).then(
+                    () => {
+                        this.props.onLoadingCompleted();
+                        this.props.onLoadedProject(this.props.loadingState, true);
+                    },
+                    e => {
+                        this.props.onLoadingCompleted();
+                        console.error(e); // TODO: dialog box reporting the error?
 
-            ipcRenderer.invoke('get-initial-project-data').then(projectData => {
-                this.setState({
-                    projectData,
-                    projectLoading: false
-                });
+                        // abandon this load and instead fetch+load the default project
+                        // WARNING: this stuff doesn't currently work correctly!
+                        this.props.onLoadedProject(this.props.loadingState, false);
+                        this.props.onRequestNewProject();
+                    }
+                );
             });
         }
         componentDidMount () {
@@ -80,15 +110,12 @@ const ScratchDesktopHOC = function (WrappedComponent) {
         render () {
             const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
 
-            if (this.state.projectLoading) {
-                return <p className="splash">Loading File...</p>;
-            }
+            const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes));
 
             return (<WrappedComponent
                 canEditTitle
+                canModifyCloudData={false}
                 isScratchDesktop
-                projectId={defaultProjectId}
-                projectTitle={this.state.projectTitle}
                 showTelemetryModal={shouldShowTelemetryModal}
                 onClickLogo={this.handleClickLogo}
                 onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
@@ -97,25 +124,60 @@ const ScratchDesktopHOC = function (WrappedComponent) {
                 onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
                 onUpdateProjectTitle={this.handleUpdateProjectTitle}
 
-                // completely omit the projectData prop if we have no project data
-                // passing an empty projectData causes a GUI error
-                {...(this.state.projectData ? {projectData: this.state.projectData} : {})}
-
                 // allow passed-in props to override any of the above
-                {...this.props}
+                {...childProps}
             />);
         }
     }
 
-    return ScratchDesktopComponent;
+    ScratchDesktopComponent.propTypes = {
+        loadingState: PropTypes.oneOf(LoadingStates),
+        onFetchedInitialProjectData: PropTypes.func,
+        onHasInitialProject: PropTypes.func,
+        onLoadedProject: PropTypes.func,
+        onLoadingCompleted: PropTypes.func,
+        onLoadingStarted: PropTypes.func,
+        onRequestNewProject: PropTypes.func,
+        vm: PropTypes.instanceOf(VM).isRequired
+    };
+    const mapStateToProps = state => {
+        const loadingState = state.scratchGui.projectState.loadingState;
+        return {
+            loadingState: loadingState,
+            vm: state.scratchGui.vm
+        };
+    };
+    const mapDispatchToProps = dispatch => ({
+        onLoadingStarted: () => dispatch(openLoadingProject()),
+        onLoadingCompleted: () => dispatch(closeLoadingProject()),
+        onHasInitialProject: (hasInitialProject, loadingState) => {
+            if (hasInitialProject) {
+                // emulate sb-file-uploader
+                return dispatch(requestProjectUpload(loadingState));
+            }
+
+            // `createProject()` might seem more appropriate but it's not a valid state transition here
+            // setting the default project ID is a valid transition from NOT_LOADED and acts like "create new"
+            return dispatch(setProjectId(defaultProjectId));
+        },
+        onFetchedInitialProjectData: (projectData, loadingState) =>
+            dispatch(onFetchedProjectData(projectData, loadingState)),
+        onLoadedProject: (loadingState, loadSuccess) => {
+            const canSaveToServer = false;
+            return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess));
+        },
+        onRequestNewProject: () => dispatch(requestNewProject(false))
+    });
+
+    return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopComponent);
 };
 
 // note that redux's 'compose' function is just being used as a general utility to make
 // the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's
 // ability to compose reducers.
 const WrappedGui = compose(
-    ScratchDesktopHOC,
-    AppStateHOC
+    AppStateHOC,
+    ScratchDesktopHOC
 )(GUI);
 
 ReactDOM.render(<WrappedGui />, appTarget);