Spaces:
Running
Running
import PropTypes from 'prop-types'; | |
import React from 'react'; | |
import {connect} from 'react-redux'; | |
import bindAll from 'lodash.bindall'; | |
import VM from 'scratch-vm'; | |
import CloudProvider from '../lib/cloud-provider'; | |
import { | |
getIsShowingWithId | |
} from '../reducers/project-state'; | |
import { | |
showAlertWithTimeout | |
} from '../reducers/alerts'; | |
import {openUsernameModal} from '../reducers/modals'; | |
import {setUsernameInvalid, setCloudHost} from '../reducers/tw'; | |
/* | |
* Higher Order Component to manage the connection to the cloud server. | |
* @param {React.Component} WrappedComponent component to manage VM events for | |
* @returns {React.Component} connected component with vm events bound to redux | |
*/ | |
const cloudManagerHOC = function (WrappedComponent) { | |
class CloudManager extends React.Component { | |
constructor (props) { | |
super(props); | |
this.cloudProvider = null; | |
bindAll(this, [ | |
'handleCloudDataUpdate', | |
'onInvalidUsername' | |
]); | |
this.props.vm.on('HAS_CLOUD_DATA_UPDATE', this.handleCloudDataUpdate); | |
this.props.onSetReduxCloudHost(this.props.cloudHost); | |
} | |
componentDidMount () { | |
if (this.shouldConnect(this.props)) { | |
this.connectToCloud(); | |
} | |
} | |
componentWillReceiveProps (nextProps) { | |
if (this.props.reduxCloudHost !== nextProps.cloudHost) { | |
this.props.onSetReduxCloudHost(nextProps.cloudHost); | |
} | |
} | |
componentDidUpdate (prevProps) { | |
// TODO need to add cloud provider disconnection logic and cloud data clearing logic | |
// when loading a new project e.g. via file upload | |
// (and eventually move it out of the vm.clear function) | |
if (this.shouldReconnect(this.props, prevProps)) { | |
this.disconnectFromCloud(); | |
if (this.shouldConnect(this.props)) { | |
this.connectToCloud(); | |
} | |
return; | |
} | |
if (this.shouldConnect(this.props) && !this.shouldConnect(prevProps)) { | |
this.connectToCloud(); | |
} | |
if (this.shouldDisconnect(this.props, prevProps)) { | |
this.disconnectFromCloud(); | |
} | |
} | |
componentWillUnmount () { | |
this.props.vm.off('HAS_CLOUD_DATA_UPDATE', this.handleCloudDataUpdate); | |
this.disconnectFromCloud(); | |
} | |
canUseCloud (props) { | |
return !!( | |
props.reduxCloudHost && | |
props.username && | |
props.vm && | |
props.projectId && | |
props.hasCloudPermission | |
); | |
} | |
shouldConnect (props) { | |
return !this.isConnected() && this.canUseCloud(props) && | |
props.isShowingWithId && props.vm.runtime.hasCloudData() && | |
props.canModifyCloudData; | |
} | |
shouldDisconnect (props, prevProps) { | |
return this.isConnected() && | |
( // Can no longer use cloud or cloud provider info is now stale | |
!this.canUseCloud(props) || | |
!props.vm.runtime.hasCloudData() || | |
(props.projectId !== prevProps.projectId) || | |
// tw: username changes are handled in "reconnect" | |
// (props.username !== prevProps.username) || | |
// Editing someone else's project | |
!props.canModifyCloudData | |
); | |
} | |
shouldReconnect (props, prevProps) { | |
return this.isConnected() && ( | |
props.username !== prevProps.username || | |
props.reduxCloudHost !== prevProps.reduxCloudHost | |
); | |
} | |
isConnected () { | |
return this.cloudProvider && !!this.cloudProvider.connection; | |
} | |
connectToCloud () { | |
this.cloudProvider = new CloudProvider( | |
this.props.reduxCloudHost, | |
this.props.vm, | |
this.props.username, | |
this.props.projectId); | |
this.cloudProvider.onInvalidUsername = this.onInvalidUsername; | |
this.props.vm.setCloudProvider(this.cloudProvider); | |
} | |
disconnectFromCloud () { | |
if (this.cloudProvider) { | |
this.cloudProvider.requestCloseConnection(); | |
this.cloudProvider = null; | |
this.props.vm.setCloudProvider(null); | |
} | |
} | |
handleCloudDataUpdate (projectHasCloudData) { | |
if (this.isConnected() && !projectHasCloudData) { | |
this.disconnectFromCloud(); | |
} else if (this.shouldConnect(this.props)) { | |
this.props.onShowCloudInfo(); | |
this.connectToCloud(); | |
} | |
} | |
onInvalidUsername () { | |
this.props.onInvalidUsername(); | |
} | |
render () { | |
const { | |
/* eslint-disable no-unused-vars */ | |
canModifyCloudData, | |
cloudHost, | |
reduxCloudHost, | |
onSetReduxCloudHost, | |
projectId, | |
username, | |
hasCloudPermission, | |
isShowingWithId, | |
onShowCloudInfo, | |
onInvalidUsername, | |
/* eslint-enable no-unused-vars */ | |
vm, | |
...componentProps | |
} = this.props; | |
return ( | |
<WrappedComponent | |
canUseCloud={this.canUseCloud(this.props)} | |
vm={vm} | |
{...componentProps} | |
/> | |
); | |
} | |
} | |
CloudManager.propTypes = { | |
canModifyCloudData: PropTypes.bool.isRequired, | |
cloudHost: PropTypes.string, | |
reduxCloudHost: PropTypes.string, | |
onSetReduxCloudHost: PropTypes.func, | |
hasCloudPermission: PropTypes.bool, | |
isShowingWithId: PropTypes.bool.isRequired, | |
onInvalidUsername: PropTypes.func, | |
onShowCloudInfo: PropTypes.func, | |
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |
username: PropTypes.string, | |
vm: PropTypes.instanceOf(VM).isRequired | |
}; | |
CloudManager.defaultProps = { | |
cloudHost: null, | |
onShowCloudInfo: () => {}, | |
username: null | |
}; | |
const mapStateToProps = (state, ownProps) => { | |
const loadingState = state.scratchGui.projectState.loadingState; | |
return { | |
reduxCloudHost: state.scratchGui.tw.cloudHost, | |
isShowingWithId: getIsShowingWithId(loadingState), | |
projectId: state.scratchGui.projectState.projectId, | |
hasCloudPermission: state.scratchGui.tw.cloud, | |
username: state.scratchGui.tw.username, | |
canModifyCloudData: (!state.scratchGui.mode.hasEverEnteredEditor || ownProps.canSave) | |
}; | |
}; | |
const mapDispatchToProps = dispatch => ({ | |
onSetReduxCloudHost: cloudHost => dispatch(setCloudHost(cloudHost)), | |
onShowCloudInfo: () => showAlertWithTimeout(dispatch, 'cloudInfo'), | |
onInvalidUsername: () => { | |
dispatch(setUsernameInvalid(true)); | |
dispatch(openUsernameModal()); | |
} | |
}); | |
// Allow incoming props to override redux-provided props. Used to mock in tests. | |
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( | |
{}, stateProps, dispatchProps, ownProps | |
); | |
return connect( | |
mapStateToProps, | |
mapDispatchToProps, | |
mergeProps | |
)(CloudManager); | |
}; | |
export default cloudManagerHOC; | |