From 19a47ecde87f7cdfe07d86569cd1f919ffef7e3f Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 17 Nov 2020 15:55:30 -0800
Subject: [PATCH 1/4] split HOC in two: one inside AppStateHOC, one out
`ScratchDesktopOuterComponent` is now responsible for
`showTelemetryModal` which only works if it comes from outside the
`AppStateHOC` since it's used in the `AppStateHOC` constructor. The
outer component also handles a few static props, like
`isScratchDesktop`.
`ScratchDesktopInnerComponent` handles everything else, most notably
anything that interacts with the state established by `AppStateHOC`.
---
src/renderer/app.jsx | 42 ++++++++++++++++++++++++++++--------------
1 file changed, 28 insertions(+), 14 deletions(-)
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 1fe1a37..4d9760d 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -7,7 +7,7 @@ import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import {compose} from 'redux';
import GUI from 'scratch-gui/src/index';
-import VM from 'scratch-vm';
+import GUIComponent from 'scratch-gui/src/components/gui/gui.jsx';
import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.jsx';
import {
@@ -43,8 +43,26 @@ document.body.appendChild(appTarget);
GUI.setAppElement(appTarget);
-const ScratchDesktopHOC = function (WrappedComponent) {
- class ScratchDesktopComponent extends React.Component {
+const ScratchDesktopOuterHOC = function (WrappedComponent) {
+ const ScratchDesktopOuterComponent = function (props) {
+ const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
+
+ return ();
+ };
+
+ return ScratchDesktopOuterComponent;
+};
+
+const ScratchDesktopInnerHOC = function (WrappedComponent) {
+ class ScratchDesktopInnerComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
@@ -116,15 +134,9 @@ const ScratchDesktopHOC = function (WrappedComponent) {
this.setState({projectTitle: newTitle});
}
render () {
- const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
-
- const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes));
+ const childProps = omit(this.props, Object.keys(ScratchDesktopInnerComponent.propTypes));
return ( {
const loadingState = state.scratchGui.projectState.loadingState;
@@ -177,15 +190,16 @@ const ScratchDesktopHOC = function (WrappedComponent) {
onRequestNewProject: () => dispatch(requestNewProject(false))
});
- return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopComponent);
+ return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopInnerComponent);
};
// 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(
+ ScratchDesktopOuterHOC,
AppStateHOC,
- ScratchDesktopHOC
+ ScratchDesktopInnerHOC
)(GUI);
ReactDOM.render(, appTarget);
From 6c00bb1539f35745ed582d64e44566f8d4ba2d59 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 17 Nov 2020 16:13:18 -0800
Subject: [PATCH 2/4] add telemetry reset instructions to README.md
---
README.md | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/README.md b/README.md
index 37433ba..55c70bc 100644
--- a/README.md
+++ b/README.md
@@ -104,3 +104,27 @@ configuration like this:
]
},
```
+
+### Resetting the Telemetry System
+
+This application includes a telemetry system which is only active if the user opts in. When testing this system, it's
+sometimes helpful to reset it by deleting the `telemetry.json` file.
+
+The location of this file depends on your operating system and whether or not you're running a packaged build. Running
+from `npm start` or equivalent is a non-packaged build.
+
+In addition, macOS may store the file in one of two places depending on the OS version and a few other variables. If
+in doubt, I recommend removing both.
+
+- Windows, packaged build: `%APPDATA%\Scratch\telemetry.json`
+- Windows, non-packaged: `%APPDATA%\Electron\telemetry.json`
+- macOS, packaged build: `~/Library/Application Support/Scratch/telemetry.json` or
+ `~/Library/Containers/edu.mit.scratch.scratch-desktop/Data/Library/Application Support/Scratch/telemetry.json`
+- macOS, non-packaged build: `~/Library/Application Support/Electron/telemetry.json` or
+ `~/Library/Containers/edu.mit.scratch.scratch-desktop/Data/Library/Application Support/Electron/telemetry.json`
+
+Deleting this file will:
+
+- Remove any pending telemetry packets
+- Reset the opt in/out state: the app should display the opt in/out modal on next launch
+- Remove the random client UUID: the app will generate a new one on next launch
From c110b13affc12cf6cf9443bda943caa25eb1471a Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Wed, 9 Dec 2020 14:02:08 -0800
Subject: [PATCH 3/4] clarify HOC names and move props to match
Previously the two HOCs in app.jsx were named according to their place
in the component structure, which didn't provide any information about
their functionality or meaning. Now they are named according to the
components they wrap, which should help with future maintenance in that
it will guide which props belong in each.
---
src/renderer/app.jsx | 36 +++++++++++++++++++++++-------------
1 file changed, 23 insertions(+), 13 deletions(-)
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 4d9760d..b868198 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -43,14 +43,16 @@ document.body.appendChild(appTarget);
GUI.setAppElement(appTarget);
-const ScratchDesktopOuterHOC = function (WrappedComponent) {
- const ScratchDesktopOuterComponent = function (props) {
+/**
+ * Higher-order component to add desktop logic to AppStateHOC.
+ * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap.
+ * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added.
+ */
+const ScratchDesktopAppStateHOC = function (WrappedComponent) {
+ const ScratchDesktopAppStateComponent = function (props) {
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
return ();
};
- return ScratchDesktopOuterComponent;
+ return ScratchDesktopAppStateComponent;
};
-const ScratchDesktopInnerHOC = function (WrappedComponent) {
- class ScratchDesktopInnerComponent extends React.Component {
+/**
+ * Higher-order component to add desktop logic to the GUI.
+ * @param {Component} WrappedComponent - a GUI-like component to wrap.
+ * @returns {Component} - a component similar to GUI with desktop-specific logic added.
+ */
+const ScratchDesktopGUIHOC = function (WrappedComponent) {
+ class ScratchDesktopGUIComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
@@ -134,9 +141,12 @@ const ScratchDesktopInnerHOC = function (WrappedComponent) {
this.setState({projectTitle: newTitle});
}
render () {
- const childProps = omit(this.props, Object.keys(ScratchDesktopInnerComponent.propTypes));
+ const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes));
return ( dispatch(requestNewProject(false))
});
- return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopInnerComponent);
+ return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent);
};
// 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(
- ScratchDesktopOuterHOC,
+ ScratchDesktopAppStateHOC,
AppStateHOC,
- ScratchDesktopInnerHOC
+ ScratchDesktopGUIHOC
)(GUI);
ReactDOM.render(, appTarget);
From 728d705b1ac6768c47c89a8297cf77c838f7f977 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 26 Jan 2021 14:17:22 -0800
Subject: [PATCH 4/4] fix merge & rearrange components to satisfy lint
---
src/renderer/ScratchDesktopAppStateHOC.jsx | 53 +++++
src/renderer/ScratchDesktopGUIHOC.jsx | 175 +++++++++++++++++
src/renderer/app.jsx | 215 +--------------------
3 files changed, 230 insertions(+), 213 deletions(-)
create mode 100644 src/renderer/ScratchDesktopAppStateHOC.jsx
create mode 100644 src/renderer/ScratchDesktopGUIHOC.jsx
diff --git a/src/renderer/ScratchDesktopAppStateHOC.jsx b/src/renderer/ScratchDesktopAppStateHOC.jsx
new file mode 100644
index 0000000..13a7905
--- /dev/null
+++ b/src/renderer/ScratchDesktopAppStateHOC.jsx
@@ -0,0 +1,53 @@
+import {ipcRenderer} from 'electron';
+import bindAll from 'lodash.bindall';
+import React from 'react';
+
+/**
+ * Higher-order component to add desktop logic to AppStateHOC.
+ * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap.
+ * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added.
+ */
+const ScratchDesktopAppStateHOC = function (WrappedComponent) {
+ class ScratchDesktopAppStateComponent extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleTelemetryModalOptIn',
+ 'handleTelemetryModalOptOut'
+ ]);
+ this.state = {
+ // use `sendSync` because this should be set before first render
+ telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn')
+ };
+ }
+ handleTelemetryModalOptIn () {
+ ipcRenderer.send('setTelemetryDidOptIn', true);
+ ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => {
+ this.setState({telemetryDidOptIn});
+ });
+ }
+ handleTelemetryModalOptOut () {
+ ipcRenderer.send('setTelemetryDidOptIn', false);
+ ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => {
+ this.setState({telemetryDidOptIn});
+ });
+ }
+ render () {
+ const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
+
+ return ();
+ }
+ }
+
+ return ScratchDesktopAppStateComponent;
+};
+
+export default ScratchDesktopAppStateHOC;
diff --git a/src/renderer/ScratchDesktopGUIHOC.jsx b/src/renderer/ScratchDesktopGUIHOC.jsx
new file mode 100644
index 0000000..97e5c9e
--- /dev/null
+++ b/src/renderer/ScratchDesktopGUIHOC.jsx
@@ -0,0 +1,175 @@
+import {ipcRenderer, remote} from 'electron';
+import bindAll from 'lodash.bindall';
+import omit from 'lodash.omit';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {connect} from 'react-redux';
+import GUIComponent from 'scratch-gui/src/components/gui/gui.jsx';
+
+import {
+ LoadingStates,
+ onFetchedProjectData,
+ onLoadedProject,
+ defaultProjectId,
+ requestNewProject,
+ requestProjectUpload,
+ setProjectId
+} from 'scratch-gui/src/reducers/project-state';
+import {
+ openLoadingProject,
+ closeLoadingProject,
+ openTelemetryModal
+} from 'scratch-gui/src/reducers/modals';
+
+import ElectronStorageHelper from '../common/ElectronStorageHelper';
+
+import showPrivacyPolicy from './showPrivacyPolicy';
+
+/**
+ * Higher-order component to add desktop logic to the GUI.
+ * @param {Component} WrappedComponent - a GUI-like component to wrap.
+ * @returns {Component} - a component similar to GUI with desktop-specific logic added.
+ */
+const ScratchDesktopGUIHOC = function (WrappedComponent) {
+ class ScratchDesktopGUIComponent extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleProjectTelemetryEvent',
+ 'handleSetTitleFromSave',
+ 'handleStorageInit',
+ 'handleUpdateProjectTitle'
+ ]);
+ 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();
+ this.props.onLoadedProject(this.props.loadingState, false);
+ remote.dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'error',
+ title: 'Failed to load project',
+ message: 'Invalid or corrupt project file.',
+ detail: e.message
+ });
+
+ // this effectively sets the default project ID
+ // TODO: maybe setting the default project ID should be implicit in `requestNewProject`
+ this.props.onHasInitialProject(false, this.props.loadingState);
+
+ // restart as if we didn't have an initial project to load
+ this.props.onRequestNewProject();
+ }
+ );
+ });
+ }
+ componentDidMount () {
+ ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
+ }
+ componentWillUnmount () {
+ ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave);
+ }
+ handleClickAbout () {
+ ipcRenderer.send('open-about-window');
+ }
+ handleProjectTelemetryEvent (event, metadata) {
+ ipcRenderer.send(event, metadata);
+ }
+ handleSetTitleFromSave (event, args) {
+ this.handleUpdateProjectTitle(args.title);
+ }
+ handleStorageInit (storageInstance) {
+ storageInstance.addHelper(new ElectronStorageHelper(storageInstance));
+ }
+ handleUpdateProjectTitle (newTitle) {
+ this.setState({projectTitle: newTitle});
+ }
+ render () {
+ const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes));
+
+ return ( this.handleClickAbout()
+ },
+ {
+ title: 'Privacy Policy',
+ onClick: () => showPrivacyPolicy()
+ },
+ {
+ title: 'Data Settings',
+ onClick: () => this.props.onTelemetrySettingsClicked()
+ }
+ ]}
+ onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
+ onShowPrivacyPolicy={showPrivacyPolicy}
+ onStorageInit={this.handleStorageInit}
+ onUpdateProjectTitle={this.handleUpdateProjectTitle}
+
+ // allow passed-in props to override any of the above
+ {...childProps}
+ />);
+ }
+ }
+
+ ScratchDesktopGUIComponent.propTypes = {
+ loadingState: PropTypes.oneOf(LoadingStates),
+ onFetchedInitialProjectData: PropTypes.func,
+ onHasInitialProject: PropTypes.func,
+ onLoadedProject: PropTypes.func,
+ onLoadingCompleted: PropTypes.func,
+ onLoadingStarted: PropTypes.func,
+ onRequestNewProject: PropTypes.func,
+ onTelemetrySettingsClicked: PropTypes.func,
+ // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch
+ vm: GUIComponent.WrappedComponent.propTypes.vm
+ };
+ 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)),
+ onTelemetrySettingsClicked: () => dispatch(openTelemetryModal())
+ });
+
+ return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent);
+};
+
+export default ScratchDesktopGUIHOC;
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 7810fc5..d437381 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -1,32 +1,11 @@
-import {ipcRenderer, remote} from 'electron';
-import bindAll from 'lodash.bindall';
-import omit from 'lodash.omit';
-import PropTypes from 'prop-types';
import React from 'react';
-import {connect} from 'react-redux';
import {compose} from 'redux';
import GUI from 'scratch-gui/src/index';
-import GUIComponent from 'scratch-gui/src/components/gui/gui.jsx';
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,
- openTelemetryModal
-} from 'scratch-gui/src/reducers/modals';
-import ElectronStorageHelper from '../common/ElectronStorageHelper';
-
-import showPrivacyPolicy from './showPrivacyPolicy';
+import ScratchDesktopAppStateHOC from './ScratchDesktopAppStateHOC.jsx';
+import ScratchDesktopGUIHOC from './ScratchDesktopGUIHOC.jsx';
import styles from './app.css';
const appTarget = document.getElementById('app');
@@ -34,196 +13,6 @@ appTarget.className = styles.app || 'app';
GUI.setAppElement(appTarget);
-/**
- * Higher-order component to add desktop logic to AppStateHOC.
- * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap.
- * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added.
- */
-const ScratchDesktopAppStateHOC = function (WrappedComponent) {
- const ScratchDesktopAppStateComponent = function (props) {
- const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
-
- return ();
- };
-
- return ScratchDesktopAppStateComponent;
-};
-
-/**
- * Higher-order component to add desktop logic to the GUI.
- * @param {Component} WrappedComponent - a GUI-like component to wrap.
- * @returns {Component} - a component similar to GUI with desktop-specific logic added.
- */
-const ScratchDesktopGUIHOC = function (WrappedComponent) {
- class ScratchDesktopGUIComponent extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleProjectTelemetryEvent',
- 'handleSetTitleFromSave',
- 'handleStorageInit',
- 'handleTelemetryModalOptIn',
- 'handleTelemetryModalOptOut',
- 'handleUpdateProjectTitle'
- ]);
- this.state = {
- // use `sendSync` because this should be set before first render
- telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn')
- };
- 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();
- this.props.onLoadedProject(this.props.loadingState, false);
- remote.dialog.showMessageBox(remote.getCurrentWindow(), {
- type: 'error',
- title: 'Failed to load project',
- message: 'Invalid or corrupt project file.',
- detail: e.message
- });
-
- // this effectively sets the default project ID
- // TODO: maybe setting the default project ID should be implicit in `requestNewProject`
- this.props.onHasInitialProject(false, this.props.loadingState);
-
- // restart as if we didn't have an initial project to load
- this.props.onRequestNewProject();
- }
- );
- });
- }
- componentDidMount () {
- ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
- }
- componentWillUnmount () {
- ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave);
- }
- handleClickAbout () {
- ipcRenderer.send('open-about-window');
- }
- handleProjectTelemetryEvent (event, metadata) {
- ipcRenderer.send(event, metadata);
- }
- handleSetTitleFromSave (event, args) {
- this.handleUpdateProjectTitle(args.title);
- }
- handleStorageInit (storageInstance) {
- storageInstance.addHelper(new ElectronStorageHelper(storageInstance));
- }
- handleTelemetryModalOptIn () {
- ipcRenderer.send('setTelemetryDidOptIn', true);
- ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => {
- this.setState({telemetryDidOptIn});
- });
- }
- handleTelemetryModalOptOut () {
- ipcRenderer.send('setTelemetryDidOptIn', false);
- ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => {
- this.setState({telemetryDidOptIn});
- });
- }
- handleUpdateProjectTitle (newTitle) {
- this.setState({projectTitle: newTitle});
- }
- render () {
- const shouldShowTelemetryModal = (typeof this.state.telemetryDidOptIn !== 'boolean');
-
- const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes));
-
- return ( this.handleClickAbout()
- },
- {
- title: 'Privacy Policy',
- onClick: () => showPrivacyPolicy()
- },
- {
- title: 'Data Settings',
- onClick: () => this.props.onTelemetrySettingsClicked()
- }
- ]}
- onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
- onShowPrivacyPolicy={showPrivacyPolicy}
- onStorageInit={this.handleStorageInit}
- onTelemetryModalOptIn={this.handleTelemetryModalOptIn}
- onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
- onUpdateProjectTitle={this.handleUpdateProjectTitle}
-
- // allow passed-in props to override any of the above
- {...childProps}
- />);
- }
- }
-
- ScratchDesktopGUIComponent.propTypes = {
- loadingState: PropTypes.oneOf(LoadingStates),
- onFetchedInitialProjectData: PropTypes.func,
- onHasInitialProject: PropTypes.func,
- onLoadedProject: PropTypes.func,
- onLoadingCompleted: PropTypes.func,
- onLoadingStarted: PropTypes.func,
- onRequestNewProject: PropTypes.func,
- onTelemetrySettingsClicked: PropTypes.func,
- // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch
- vm: GUIComponent.WrappedComponent.propTypes.vm
- };
- 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)),
- onTelemetrySettingsClicked: () => dispatch(openTelemetryModal())
- });
-
- return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent);
-};
// 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