penguinmod-editor-2 / src /lib /cloud-manager-hoc.jsx
soiz1's picture
Upload 2891 files
6bcb42f verified
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;