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 ( ); } } 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;