-
Notifications
You must be signed in to change notification settings - Fork 174
Add exam mode #3106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add exam mode #3106
Changes from all commits
1ec4ff9
1aba138
5111ee5
c8d8f18
804fdad
7f09b05
38b208c
f17ff59
bad7d50
3f4921b
2d837c1
0c7526b
fc1a803
6a20fc7
736e624
1f42ded
6cd6727
b72a22b
9f5e3a2
feacd38
cc13931
7d61c43
23415c9
0c68331
ad49ef5
c52f215
d8e3d6f
a9cfd33
15a9e62
666f50a
1bdac8e
bedcc4d
0b28757
aa1e266
d91e7af
685184d
5caf874
0e3101f
d6c2ca3
3f3b75d
a525978
ec9c372
2777ebf
bab2a81
669a347
bb4c32d
3e660d9
50eb143
87feefe
80d57bf
ef6f91c
1447ed5
39ec7f8
37c947e
95469a5
a8b1b4b
8a7671b
a244844
5289a9d
3cb1957
52635e8
bcb9833
a0e854d
75a9fbb
715aeff
407052d
1b21039
6319405
9901efa
b1f91c1
62c3a8c
156e3a3
7dcb157
0523f69
f5b3012
a5f524e
25b823b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import React from 'react'; | ||
| import disableDevtool from 'disable-devtool'; | ||
| import React, { useState } from 'react'; | ||
| import { useDispatch } from 'react-redux'; | ||
| import { Outlet } from 'react-router'; | ||
| import Messages, { | ||
|
|
@@ -8,16 +9,19 @@ import Messages, { | |
| } from 'src/features/vscode/messages'; | ||
|
|
||
| import NavigationBar from '../navigationBar/NavigationBar'; | ||
| import { PauseAcademyOverlay } from '../pauseAcademyOverlay/PauseAcademyOverlay'; | ||
| import Constants from '../utils/Constants'; | ||
| import { useLocalStorageState, useSession } from '../utils/Hooks'; | ||
| import { showDangerMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; | ||
| import WorkspaceActions from '../workspace/WorkspaceActions'; | ||
| import { defaultWorkspaceSettings, WorkspaceSettingsContext } from '../WorkspaceSettingsContext'; | ||
| import SessionActions from './actions/SessionActions'; | ||
| import VscodeActions from './actions/VscodeActions'; | ||
| import { Role } from './ApplicationTypes'; | ||
|
|
||
| const Application: React.FC = () => { | ||
| const dispatch = useDispatch(); | ||
| const { isLoggedIn } = useSession(); | ||
| const { isLoggedIn, isPaused, enableExamMode } = useSession(); | ||
|
|
||
| // Used in the mobile/PWA experience (e.g. separate handling of orientation changes on Andriod & iOS due to unique browser behaviours) | ||
| const isMobile = /iPhone|iPad|Android/.test(navigator.userAgent); | ||
|
|
@@ -29,6 +33,16 @@ const Application: React.FC = () => { | |
| defaultWorkspaceSettings | ||
| ); | ||
|
|
||
| // Used for dev tools detection | ||
| const [isPreviewExamMode] = useLocalStorageState( | ||
| Constants.isPreviewExamModeLocalStorageKey, | ||
| false | ||
| ); | ||
| const [pauseAcademy, setPauseAcademy] = useState(false); | ||
| const [pauseAcademyReason, setPauseAcademyReason] = useState(''); | ||
| const hasSentPauseUserRequest = React.useRef<boolean>(false); | ||
| const { role } = useSession(); | ||
|
|
||
| // Effect to fetch the latest user info and course configurations from the backend on refresh, | ||
| // if the user was previously logged in | ||
| React.useEffect(() => { | ||
|
|
@@ -153,14 +167,90 @@ const Application: React.FC = () => { | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
||
| // Effect for dev tools blocking/detection when exam mode enabled | ||
| React.useEffect(() => { | ||
| if (role !== Role.Student && !isPreviewExamMode) { | ||
| return; | ||
| } | ||
|
|
||
| const showPauseAcademyOverlay = (reason: string) => { | ||
| setPauseAcademy(true); | ||
| setPauseAcademyReason(reason); | ||
| if (hasSentPauseUserRequest.current === false) { | ||
| dispatch(SessionActions.pauseUser()); | ||
| hasSentPauseUserRequest.current = true; | ||
| } | ||
| }; | ||
|
|
||
| if (enableExamMode || isPreviewExamMode) { | ||
| dispatch(SessionActions.reportFocusRegain()); | ||
|
|
||
| if (isPaused !== undefined && isPaused) { | ||
| showPauseAcademyOverlay('Browser was refreshed when Source Academy was paused'); | ||
| } else { | ||
| hasSentPauseUserRequest.current = false; | ||
| } | ||
|
|
||
| // Disable/Detect dev tools | ||
| disableDevtool({ | ||
| ondevtoolopen: () => { | ||
| showPauseAcademyOverlay('Developer tools detected'); | ||
| } | ||
| }); | ||
|
|
||
| document.addEventListener('contextmenu', event => event.preventDefault()); | ||
| document.addEventListener('keydown', event => { | ||
| if ( | ||
| event.key === 'F12' || | ||
| ((event.key === 'I' || event.key === 'J' || event.key === 'C') && | ||
| event.ctrlKey && | ||
| event.shiftKey) | ||
| ) { | ||
| event.preventDefault(); | ||
| } | ||
| }); | ||
|
|
||
| // Detect when Source Academy tab's content are hidden (e.g., user changes tab while Source Academy is active) | ||
| document.addEventListener('visibilitychange', _ => { | ||
| if (document.visibilityState === 'hidden') { | ||
| dispatch(SessionActions.reportFocusLost()); | ||
| } else { | ||
| showDangerMessage('Source Academy was out of focus.', 5000); | ||
| dispatch(SessionActions.reportFocusRegain()); | ||
| } | ||
| }); | ||
|
Comment on lines
169
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dev tools detection effect (lines 169-221) adds event listeners to the document without ever removing them. This causes memory leaks when the component unmounts or when the effect re-runs. Each time the effect runs, new listeners are added without removing the old ones, potentially accumulating dozens of event listeners. Add a cleanup function that removes all listeners, or better yet, use AbortController or WeakMaps to track and manage listeners. 🤖 Prompt for AI Agent Did we get this right? 👍 / 👎 to inform future reviews. |
||
| } | ||
| }, [dispatch, enableExamMode, isPaused, hasSentPauseUserRequest, role, isPreviewExamMode]); | ||
|
Comment on lines
+213
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The 🔍 Detailed AnalysisThe 💡 Suggested FixAdd a cleanup function to the 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
|
|
||
| const resumeCodeSubmitHandler = (resumeCode: string) => { | ||
| if (!resumeCode || resumeCode.length === 0) { | ||
| showWarningMessage('Resume code cannot be empty.'); | ||
| } else { | ||
| dispatch( | ||
| SessionActions.validateResumeCode(resumeCode, (isResumeCodeValid: boolean) => { | ||
| if (isResumeCodeValid) { | ||
| setPauseAcademy(false); | ||
| hasSentPauseUserRequest.current = false; | ||
| } else { | ||
| showWarningMessage('Resume code is invalid.'); | ||
| } | ||
| }) | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <WorkspaceSettingsContext.Provider value={[workspaceSettings, setWorkspaceSettings]}> | ||
| <div className="Application"> | ||
| <NavigationBar /> | ||
| <div className="Application__main"> | ||
| <Outlet /> | ||
| {pauseAcademy ? ( | ||
| <PauseAcademyOverlay reason={pauseAcademyReason} onSubmit={resumeCodeSubmitHandler} /> | ||
| ) : ( | ||
| <div className="Application"> | ||
| <NavigationBar /> | ||
| <div className="Application__main"> | ||
| <Outlet /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </WorkspaceSettingsContext.Provider> | ||
| ); | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exam mode check on line 170 uses
role !== Role.Student && !isPreviewExamModewhich returns early. This means non-students won't set up dev tools detection unless in preview mode. The logic seems backwards - you probably want to trigger detection for students OR when in preview mode. The condition should likely beif (role === Role.Student || isPreviewExamMode)before setting up the detection. Please verify the intended behavior.Severity: MEDIUM
🤖 Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.