\n
\n \n {` `}\n \n \n \n .\n
\n
\n \n
\n
\n \n {` `}\n \n \n \n {` `}\n \n
\n
\n \n {` `}\n \n \n \n ,{` `}\n \n \n \n {` `}\n \n {` `}\n \n \n \n {` `}\n \n
\n
\n \n {!feedbackSubmitted && (formOpen ? feedbackForm : openFormButton)}\n {feedbackSubmitted && submittedMessage}\n
\n );\n }\n}\n\nUserFeedback.propTypes = {\n onSubmit: PropTypes.func.isRequired\n};\n\nexport default UserFeedback;\n","/* global ReactOnRails:false */\n\nimport React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimport { FormattedMessage } from 'react-intl';\nimport get from 'lodash.get';\nimport find from 'lodash.find';\n\nimport {\n runShape,\n screenShape,\n sourceShape,\n chapterShape,\n exerciseShape\n} from '../../../shapes/entities';\nimport { modeType } from '../../../shapes/types';\n\nimport ChapterProgressBar from './chapter_progress_bar';\nimport ConnectedFocusTestScreen from '../focus_test';\nimport ConnectedOfflineFocusTestScreen from '../offline_focus_test';\nimport ConnectedInfoPageScreen from '../info_page';\nimport ConnectedQuestionBlockScreen from '../question_block';\nimport ConnectedQuestionScreen from '../question';\n\nimport Countdown from '../../../components/countdown';\nimport Controls from '../../../components/controls';\nimport ControlItem from '../../../components/control_item';\nimport ConnectedNextButton from '../../../components/next_button';\nimport UserFeedback from '../../../components/user_feedback';\n\nimport { getExerciseScreenProps } from '../../../selectors/player';\n\nimport {\n open,\n expireChapter,\n endChapter,\n instantAnswer\n} from '../../../actions/player';\nimport {\n chapterExpiresAt,\n chapterAccessibleAt,\n setChapterExpiryTimeout,\n setChapterAccessTimeout\n} from '../../../utils/chapter_timer';\n\nimport * as modes from '../../../modes';\nimport * as screenTypes from '../../../screens';\n\nconst map = {\n info_pages: ConnectedInfoPageScreen,\n focus_test_settings: ConnectedFocusTestScreen,\n offline_focus_test_sections: ConnectedOfflineFocusTestScreen,\n question_blocks: ConnectedQuestionBlockScreen,\n questions: ConnectedQuestionScreen\n};\n\nconst screenChapterId = screen =>\n get(screen, ['relationships', 'chapter', 'data', 'id']);\n\nconst nextScreenHasBarrier = ({\n mode,\n chapter,\n nextScreenChapter,\n nextScreenChapterEntryBarrier\n}) =>\n nextScreenChapter &&\n mode === modes.playing &&\n chapter.id !== nextScreenChapter.id &&\n chapterAccessibleAt(nextScreenChapter, nextScreenChapterEntryBarrier) >=\n Date.now();\n\nexport class ExerciseScreen extends Component {\n constructor(props) {\n super(props);\n\n this.state = {\n nextEnabled: !nextScreenHasBarrier(props),\n nextBlocked: false,\n onScreenLeaveCallbacks: []\n };\n\n this.openScreen = this.openScreen.bind(this);\n this.setChapterExpiryTimer = this.setChapterExpiryTimer.bind(this);\n this.setChapterAccessTimer = this.setChapterAccessTimer.bind(this);\n this.timeoutChapter = this.timeoutChapter.bind(this);\n this.openNextScreen = this.openNextScreen.bind(this);\n this.onScreenLeave = this.onScreenLeave.bind(this);\n this.executeOnScreenLeaveCallbacks = this.executeOnScreenLeaveCallbacks.bind(\n this\n );\n this.submitAnswerForInstantSolution = this.submitAnswerForInstantSolution.bind(\n this\n );\n this.openSolutionsGrid = this.openSolutionsGrid.bind(this);\n this.openPreviousScreen = this.openPreviousScreen.bind(this);\n this.openExerciseScreen = this.openExerciseScreen.bind(this);\n this.submitUserFeedback = this.submitUserFeedback.bind(this);\n this.toggleBlockNext = this.toggleBlockNext.bind(this);\n }\n\n componentDidMount() {\n const { mode } = this.props;\n if (mode !== modes.playing) return;\n this.setChapterExpiryTimer();\n this.setChapterAccessTimer();\n }\n\n componentWillUnmount() {\n if (this.expireTimer) clearTimeout(this.expireTimer);\n if (this.accessTimer) clearTimeout(this.accessTimer);\n }\n\n onScreenLeave(callback) {\n const { onScreenLeaveCallbacks } = this.state;\n this.setState({\n onScreenLeaveCallbacks: onScreenLeaveCallbacks.concat([callback])\n });\n }\n\n setChapterExpiryTimer() {\n const { chapter } = this.props;\n this.expireTimer = setChapterExpiryTimeout(chapter, this.timeoutChapter);\n }\n\n setChapterAccessTimer() {\n const { nextEnabled } = this.state;\n if (nextEnabled) return;\n const { nextScreenChapter, nextScreenChapterEntryBarrier } = this.props;\n this.accessTimer = setChapterAccessTimeout(\n nextScreenChapter,\n nextScreenChapterEntryBarrier,\n () => this.setState({ nextEnabled: true })\n );\n }\n\n openScreen(screen) {\n const { run, open: navigate } = this.props;\n if (screen) navigate(run, screen);\n }\n\n timeoutChapter() {\n const { run, chapter, expireChapter: timeout } = this.props;\n if (chapter) timeout(run, chapter);\n }\n\n openScreenAt(index) {\n const { screens } = this.props;\n const screen = screens[index];\n this.openScreen(screen);\n }\n\n toggleBlockNext() {\n const { nextBlocked } = this.state;\n this.setState({ nextBlocked: !nextBlocked });\n }\n\n executeOnScreenLeaveCallbacks() {\n const { onScreenLeaveCallbacks } = this.state;\n onScreenLeaveCallbacks.forEach(callback => callback());\n }\n\n openNextScreen() {\n const {\n run,\n index,\n chapter,\n nextScreenChapter,\n endChapter: move\n } = this.props;\n\n // some exercise screens (e.g. the focus_test) need to get a chance\n // to finish computation (sending events to the backend) _before_\n // we navigate away and schedule our navigation-events.\n this.executeOnScreenLeaveCallbacks();\n\n if (nextScreenChapter && chapter.id !== nextScreenChapter.id) {\n move(run, chapter);\n }\n this.openScreenAt(index + 1);\n }\n\n submitAnswerForInstantSolution() {\n const { instantAnswer: submitInstantAnswer, run, exercise } = this.props;\n submitInstantAnswer(run, exercise);\n }\n\n openPreviousScreen() {\n const { index } = this.props;\n this.openScreenAt(index - 1);\n }\n\n openSolutionsGrid() {\n const { screens } = this.props;\n const target = find(screens, {\n attributes: { subtype: screenTypes.solutions }\n });\n if (target) this.openScreen(target);\n }\n\n openExerciseScreen(exercise) {\n const { screens } = this.props;\n const { id, type } = exercise;\n const matcher = { relationships: { exercise: { data: { id, type } } } };\n const target = find(screens, matcher);\n this.openScreen(target);\n }\n\n submitUserFeedback({ message }) {\n const method = 'POST';\n const url = '/api/v1/user_feedbacks';\n const headers = {\n ...ReactOnRails.authenticityHeaders(),\n 'Content-Type': 'application/json',\n Accept: 'application/json'\n };\n const { exercise } = this.props;\n const body = JSON.stringify({\n user_feedback: {\n message,\n exercise_id: exercise.id\n }\n });\n\n const options = { method, headers, body, credentials: 'same-origin' };\n\n return fetch(url, options).then(response => {\n if (response.ok) return response;\n const error = new Error('UserFeedback Error');\n error.response = response;\n throw error;\n });\n }\n\n render() {\n const {\n run,\n mode,\n screens,\n index,\n exercises,\n exercise,\n source,\n chapter\n } = this.props;\n\n const { instant_solution: instantSolutionActive } = run.attributes;\n const {\n instant_answer_submitted_at: exerciseInstantAnswered\n } = exercise.attributes;\n\n const { nextEnabled, nextBlocked } = this.state;\n\n const ExerciseScreenContainer = map[source.type];\n const chapterExpiryDate = chapterExpiresAt(chapter);\n\n const showNext = index < screens.length - 1;\n const showPrev =\n index > 0 &&\n (run.attributes.type === 'LessonTrail' ||\n mode === modes.review ||\n screenChapterId(screens[index - 1]) ===\n screenChapterId(screens[index]));\n const showSolutions = mode === modes.review;\n const instantAnswerCheckPossible =\n mode === modes.playing &&\n instantSolutionActive &&\n source.type === 'questions' &&\n !exerciseInstantAnswered;\n\n const nextButton = showNext && (\n