diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 3eef5b1a3..14b7c7805 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -31,6 +31,7 @@ import Cards from '../../containers/cards.jsx'; import Alerts from '../../containers/alerts.jsx'; import DragLayer from '../../containers/drag-layer.jsx'; import ConnectionModal from '../../containers/connection-modal.jsx'; +import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; import {resolveStageSize} from '../../lib/screen-utils'; @@ -100,6 +101,7 @@ const GUIComponent = props => { onExtensionButtonClick, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, + onRequestCloseTelemetryModal, onSeeCommunity, onShare, previewInfoVisible, @@ -107,6 +109,8 @@ const GUIComponent = props => { soundsTabVisible, stageSizeMode, targetIsStage, + telemetryModalHandlers, + telemetryModalVisible, tipsLibraryVisible, vm, ...componentProps @@ -151,6 +155,12 @@ const GUIComponent = props => { {previewInfoVisible ? ( ) : null} + {telemetryModalVisible ? ( + + ) : null} {loading ? ( ) : null} @@ -379,6 +389,7 @@ GUIComponent.propTypes = { onOpenRegistration: PropTypes.func, onRequestCloseBackdropLibrary: PropTypes.func, onRequestCloseCostumeLibrary: PropTypes.func, + onRequestCloseTelemetryModal: PropTypes.func, onSeeCommunity: PropTypes.func, onShare: PropTypes.func, onTabSelect: PropTypes.func, @@ -390,6 +401,12 @@ GUIComponent.propTypes = { soundsTabVisible: PropTypes.bool, stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), targetIsStage: PropTypes.bool, + telemetryModalHandlers: PropTypes.shape({ + onCancel: PropTypes.func, + onOptIn: PropTypes.func.isRequired, + onOptOut: PropTypes.func.isRequired + }), + telemetryModalVisible: PropTypes.bool, tipsLibraryVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/components/telemetry-modal/telemetry-modal-header.png b/src/components/telemetry-modal/telemetry-modal-header.png new file mode 100644 index 000000000..b297b108e Binary files /dev/null and b/src/components/telemetry-modal/telemetry-modal-header.png differ diff --git a/src/components/telemetry-modal/telemetry-modal.css b/src/components/telemetry-modal/telemetry-modal.css new file mode 100644 index 000000000..18b8d1ed8 --- /dev/null +++ b/src/components/telemetry-modal/telemetry-modal.css @@ -0,0 +1,82 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; +@import "../../css/z-index.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: $z-index-modal; + background-color: $ui-modal-overlay; +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid $ui-white-transparent; + padding: 0; + border-radius: $space; + user-select: none; + width: 640px; + + color: $text-primary; + overflow: hidden; +} + +.illustration { + width: 100%; + height: 128px; + background-color: $motion-primary; + background-image: url('./telemetry-modal-header.png'); + background-size: cover; +} + +.body { + background: $ui-white; + padding: 1.5rem 2.25rem; + text-align: left; +} + +.privacy-policy-link { + color: $motion-primary; + text-decoration: none; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 1.5rem; + background: white; + font-weight: bold; + font-size: .875rem; + cursor: pointer; +} + +.button-row button.opt-in { + background: $motion-primary; + color: white; +} + +.button-row button.opt-out { + color: $motion-primary; +} + +[dir="ltr"] .button-row button + button { + margin-left: 0.5rem; +} + +[dir="rtl"] .button-row button + button { + margin-right: 0.5rem; +} diff --git a/src/components/telemetry-modal/telemetry-modal.jsx b/src/components/telemetry-modal/telemetry-modal.jsx new file mode 100644 index 000000000..b9e805aaf --- /dev/null +++ b/src/components/telemetry-modal/telemetry-modal.jsx @@ -0,0 +1,136 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import ReactModal from 'react-modal'; + +import Box from '../box/box.jsx'; + +import styles from './telemetry-modal.css'; + +const messages = defineMessages({ + label: { + id: 'gui.telemetryOptIn.label', + defaultMessage: 'Report statistics to improve Scratch', + description: 'Scratch 3.0 telemetry modal label - for accessibility' + }, + bodyText1: { + defaultMessage: 'The Scratch Team is always looking to better understand how Scratch is used around the ' + + 'world. To help support this effort, you can allow Scratch to automatically send usage information to ' + + 'the Scratch Team.', + description: 'First paragraph of body text for telemetry opt-in modal', + id: 'gui.telemetryOptIn.body1' + }, + bodyText2: { + defaultMessage: 'The information we collect includes language selection, blocks usage, and some events like ' + + 'saving, loading, and uploading a project. We DO NOT collect any personal information. Please see our ' + + '{privacyPolicyLink} for more information.', + description: 'First paragraph of body text for telemetry opt-in modal', + id: 'gui.telemetryOptIn.body2' + }, + privacyPolicyLink: { + defaultMessage: 'Privacy Policy', + description: 'Link to the Scratch privacy policy', + id: 'gui.telemetryOptIn.privacyPolicyLink' + }, + noButton: { + defaultMessage: 'No, thanks', + description: 'Text for telemetry modal opt-out button', + id: 'gui.telemetryOptIn.buttonTextNo' + }, + noTooltip: { + defaultMessage: 'Disable telemetry', + description: 'Tooltip for telemetry modal opt-out button', + id: 'gui.telemetryOptIn.buttonTooltipNo' + }, + yesButton: { + defaultMessage: "Yes, I'd like to help improve Scratch", + description: 'Text for telemetry modal opt-in button', + id: 'gui.telemetryOptIn.buttonTextYes' + }, + yesTooltip: { + defaultMessage: 'Enable telemetry', + description: 'Tooltip for telemetry modal opt-in button', + id: 'gui.telemetryOptIn.buttonTooltipYes' + } +}); + +class TelemetryModal extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleCancel', + 'handleOptIn', + 'handleOptOut' + ]); + } + handleCancel () { + this.props.onRequestClose(); + if (this.props.onCancel) { + this.props.onCancel(); + } + } + handleOptIn () { + this.props.onRequestClose(); + this.props.onOptIn(); + } + handleOptOut () { + this.props.onRequestClose(); + this.props.onOptOut(); + } + render () { + return ( +
+ + + +

+

+ + ) + }} + />

+ + + + +
+
+
); + } +} + +TelemetryModal.propTypes = { + intl: intlShape.isRequired, + isRtl: PropTypes.bool, + onCancel: PropTypes.func, + onOptIn: PropTypes.func.isRequired, + onOptOut: PropTypes.func.isRequired, + onRequestClose: PropTypes.func +}; + +export default injectIntl(TelemetryModal); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 025a73966..4011ba271 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -7,7 +7,6 @@ import VM from 'scratch-vm'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; -import {openExtensionLibrary} from '../reducers/modals'; import { getIsError, getIsShowingProject @@ -22,7 +21,9 @@ import { import { closeCostumeLibrary, - closeBackdropLibrary + closeBackdropLibrary, + closeTelemetryModal, + openExtensionLibrary } from '../reducers/modals'; import FontLoaderHOC from '../lib/font-loader-hoc.jsx'; @@ -128,6 +129,7 @@ GUI.propTypes = { projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), projectTitle: PropTypes.string, + telemetryModalVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; @@ -157,11 +159,12 @@ const mapStateToProps = state => { loadingStateVisible: state.scratchGui.modals.loadingProject, previewInfoVisible: state.scratchGui.modals.previewInfo, projectId: state.scratchGui.projectState.projectId, + soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, targetIsStage: ( state.scratchGui.targets.stage && state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget ), - soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, + telemetryModalVisible: state.scratchGui.modals.telemetryModal, tipsLibraryVisible: state.scratchGui.modals.tipsLibrary, vm: state.scratchGui.vm }; @@ -174,6 +177,7 @@ const mapDispatchToProps = dispatch => ({ onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)), onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), + onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()), onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title)) }); diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index 4156cd094..eacf2eab6 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -51,7 +51,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { guiMiddleware, initFullScreen, initPlayer, - initPreviewInfo + initPreviewInfo, + initTelemetryModal } = guiRedux; const {ScratchPaintReducer} = require('scratch-paint'); @@ -63,6 +64,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { if (props.isPlayerOnly) { initializedGui = initPlayer(initializedGui); } + } else if (props.showTelemetryModal) { + initializedGui = initTelemetryModal(initializedGui); } else if (props.showPreviewInfo) { initializedGui = initPreviewInfo(initializedGui); } @@ -98,6 +101,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { isFullScreen, // eslint-disable-line no-unused-vars isPlayerOnly, // eslint-disable-line no-unused-vars showPreviewInfo, // eslint-disable-line no-unused-vars + showTelemetryModal, // eslint-disable-line no-unused-vars ...componentProps } = this.props; return ( diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index 3ae35e4d3..ab67cd2c1 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -6,11 +6,24 @@ import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; import TitledHOC from '../lib/titled-hoc.jsx'; +import log from '../lib/log.js'; const onClickLogo = () => { window.location = 'https://scratch.mit.edu'; }; +const handleTelemetryModalCancel = () => { + log('User canceled telemetry modal'); +}; + +const handleTelemetryModalOptIn = () => { + log('User opted into telemetry'); +}; + +const handleTelemetryModalOptOut = () => { + log('User opted out of telemetry'); +}; + /* * Render the GUI playground. This is a separate function because importing anything * that instantiates the VM causes unsupported browsers to crash @@ -55,7 +68,13 @@ export default appTarget => { simulateScratchDesktop ? :