mirror of
https://github.com/scratchfoundation/scratch-desktop.git
synced 2024-12-23 06:02:30 -05:00
Merge pull request #166 from cwillisf/fix-telemetry-modal
Fix telemetry modal
This commit is contained in:
commit
c8c9ae51f4
4 changed files with 256 additions and 188 deletions
24
README.md
24
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
|
||||||
|
|
53
src/renderer/ScratchDesktopAppStateHOC.jsx
Normal file
53
src/renderer/ScratchDesktopAppStateHOC.jsx
Normal file
|
@ -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 (<WrappedComponent
|
||||||
|
isTelemetryEnabled={this.state.telemetryDidOptIn}
|
||||||
|
onTelemetryModalOptIn={this.handleTelemetryModalOptIn}
|
||||||
|
onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
|
||||||
|
showTelemetryModal={shouldShowTelemetryModal}
|
||||||
|
|
||||||
|
// allow passed-in props to override any of the above
|
||||||
|
{...this.props}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScratchDesktopAppStateComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScratchDesktopAppStateHOC;
|
175
src/renderer/ScratchDesktopGUIHOC.jsx
Normal file
175
src/renderer/ScratchDesktopGUIHOC.jsx
Normal file
|
@ -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 (<WrappedComponent
|
||||||
|
canEditTitle
|
||||||
|
canModifyCloudData={false}
|
||||||
|
canSave={false}
|
||||||
|
isScratchDesktop
|
||||||
|
onClickAbout={[
|
||||||
|
{
|
||||||
|
title: 'About',
|
||||||
|
onClick: () => 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;
|
|
@ -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 React from 'react';
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {compose} from 'redux';
|
import {compose} from 'redux';
|
||||||
import GUI from 'scratch-gui/src/index';
|
import GUI from 'scratch-gui/src/index';
|
||||||
import VM from 'scratch-vm';
|
|
||||||
|
|
||||||
import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.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 ScratchDesktopAppStateHOC from './ScratchDesktopAppStateHOC.jsx';
|
||||||
|
import ScratchDesktopGUIHOC from './ScratchDesktopGUIHOC.jsx';
|
||||||
import showPrivacyPolicy from './showPrivacyPolicy';
|
|
||||||
import styles from './app.css';
|
import styles from './app.css';
|
||||||
|
|
||||||
const appTarget = document.getElementById('app');
|
const appTarget = document.getElementById('app');
|
||||||
|
@ -34,177 +13,14 @@ appTarget.className = styles.app || 'app';
|
||||||
|
|
||||||
GUI.setAppElement(appTarget);
|
GUI.setAppElement(appTarget);
|
||||||
|
|
||||||
const ScratchDesktopHOC = function (WrappedComponent) {
|
|
||||||
class ScratchDesktopComponent 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(ScratchDesktopComponent.propTypes));
|
|
||||||
|
|
||||||
return (<WrappedComponent
|
|
||||||
canEditTitle
|
|
||||||
canModifyCloudData={false}
|
|
||||||
canSave={false}
|
|
||||||
isScratchDesktop
|
|
||||||
isTelemetryEnabled={this.state.telemetryDidOptIn}
|
|
||||||
showTelemetryModal={shouldShowTelemetryModal}
|
|
||||||
onClickAbout={[
|
|
||||||
{
|
|
||||||
title: 'About',
|
|
||||||
onClick: () => 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}
|
|
||||||
/>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScratchDesktopComponent.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,
|
|
||||||
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)),
|
|
||||||
onTelemetrySettingsClicked: () => dispatch(openTelemetryModal())
|
|
||||||
});
|
|
||||||
|
|
||||||
return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopComponent);
|
|
||||||
};
|
|
||||||
|
|
||||||
// note that redux's 'compose' function is just being used as a general utility to make
|
// 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
|
// the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's
|
||||||
// ability to compose reducers.
|
// ability to compose reducers.
|
||||||
const WrappedGui = compose(
|
const WrappedGui = compose(
|
||||||
|
ScratchDesktopAppStateHOC,
|
||||||
AppStateHOC,
|
AppStateHOC,
|
||||||
ScratchDesktopHOC
|
ScratchDesktopGUIHOC
|
||||||
)(GUI);
|
)(GUI);
|
||||||
|
|
||||||
export default <WrappedGui />;
|
export default <WrappedGui />;
|
||||||
|
|
Loading…
Reference in a new issue