From e5e41ce92afd50a32eef8e3889315ba4f56d4eaa Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Thu, 7 Nov 2024 14:07:00 +0800 Subject: [PATCH 1/2] fix(frontend): :bug: mounts component before accessing local storage Only access localStorage on the client side (in the browser) after the component has mounted. --- .../src/app/collaboration/[id]/page.tsx | 137 +++++++++++------- .../CollaborativeEditor.tsx | 47 +++--- .../src/components/VideoPanel/VideoPanel.tsx | 18 ++- 3 files changed, 118 insertions(+), 84 deletions(-) diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 15c9c5dcd4..caa7469eea 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -31,7 +31,18 @@ import CollaborativeEditor, { } from "@/components/CollaborativeEditor/CollaborativeEditor"; import { CreateHistory } from "@/app/services/history"; import { WebrtcProvider } from "y-webrtc"; -import { ExecuteVisibleAndCustomTests, ExecuteVisibleAndHiddenTestsAndSubmit, ExecutionResults, GetVisibleTests, isTestResult, SubmissionHiddenTestResultsAndStatus, SubmissionResults, Test, TestData, TestResult } from "@/app/services/execute"; +import { + ExecuteVisibleAndCustomTests, + ExecuteVisibleAndHiddenTestsAndSubmit, + ExecutionResults, + GetVisibleTests, + isTestResult, + SubmissionHiddenTestResultsAndStatus, + SubmissionResults, + Test, + TestData, + TestResult, +} from "@/app/services/execute"; import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; import VideoPanel from "@/components/VideoPanel/VideoPanel"; @@ -69,15 +80,17 @@ export default function CollaborationPage(props: CollaborationProps) { ); const [currentUser, setCurrentUser] = useState(undefined); const [matchedUser, setMatchedUser] = useState("Loading..."); - const [sessionDuration, setSessionDuration] = useState(() => { - const storedTime = localStorage.getItem("session-duration"); - return storedTime ? parseInt(storedTime) : 0; - }); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future) + const [sessionDuration, setSessionDuration] = useState(0); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future) const stopwatchRef = useRef(null); const [matchedTopics, setMatchedTopics] = useState( undefined ); + useEffect(() => { + const storedTime = localStorage.getItem("session-duration"); + setSessionDuration(storedTime ? parseInt(storedTime) : 0); + }, []); + // Chat states const [messageToSend, setMessageToSend] = useState( undefined @@ -89,8 +102,12 @@ export default function CollaborationPage(props: CollaborationProps) { ); const [visibleTestCases, setVisibleTestCases] = useState([]); const [isLoadingTestCase, setIsLoadingTestCase] = useState(false); - const [isLoadingSubmission, setIsLoadingSubmission] = useState(false); - const [submissionHiddenTestResultsAndStatus, setSubmissionHiddenTestResultsAndStatus] = useState(undefined); + const [isLoadingSubmission, setIsLoadingSubmission] = + useState(false); + const [ + submissionHiddenTestResultsAndStatus, + setSubmissionHiddenTestResultsAndStatus, + ] = useState(undefined); // End Button Modal state const [isModalOpen, setIsModalOpen] = useState(false); @@ -151,7 +168,7 @@ export default function CollaborationPage(props: CollaborationProps) { type: "info", content: message, }); - } + }; const sendSubmissionResultsToMatchedUser = (data: SubmissionResults) => { if (!providerRef.current) { @@ -161,7 +178,7 @@ export default function CollaborationPage(props: CollaborationProps) { submissionResults: data, id: Date.now(), }); - } + }; const sendExecutingStateToMatchedUser = (executing: boolean) => { if (!providerRef.current) { @@ -171,7 +188,7 @@ export default function CollaborationPage(props: CollaborationProps) { executing: executing, id: Date.now(), }); - } + }; const sendSubmittingStateToMatchedUser = (submitting: boolean) => { if (!providerRef.current) { @@ -181,7 +198,7 @@ export default function CollaborationPage(props: CollaborationProps) { submitting: submitting, id: Date.now(), }); - } + }; const sendExecutionResultsToMatchedUser = (data: ExecutionResults) => { if (!providerRef.current) { @@ -191,7 +208,7 @@ export default function CollaborationPage(props: CollaborationProps) { executionResults: data, id: Date.now(), }); - } + }; const updateSubmissionResults = (data: SubmissionResults) => { setSubmissionHiddenTestResultsAndStatus({ @@ -199,11 +216,11 @@ export default function CollaborationPage(props: CollaborationProps) { status: data.status, }); setVisibleTestCases(data.visibleTestResults); - } + }; const updateExecutionResults = (data: ExecutionResults) => { setVisibleTestCases(data.visibleTestResults); - } + }; const handleRunTestCases = async () => { if (!questionDocRefId) { @@ -211,20 +228,17 @@ export default function CollaborationPage(props: CollaborationProps) { } setIsLoadingTestCase(true); sendExecutingStateToMatchedUser(true); - const data = await ExecuteVisibleAndCustomTests( - questionDocRefId, - { - code: code, - language: selectedLanguage, - customTestCases: "", - } - ); + const data = await ExecuteVisibleAndCustomTests(questionDocRefId, { + code: code, + language: selectedLanguage, + customTestCases: "", + }); setVisibleTestCases(data.visibleTestResults); - infoMessage("Test cases executed. Review the results below.") + infoMessage("Test cases executed. Review the results below."); sendExecutionResultsToMatchedUser(data); setIsLoadingTestCase(false); sendExecutingStateToMatchedUser(false); - } + }; const handleSubmitCode = async () => { if (!questionDocRefId) { @@ -232,19 +246,16 @@ export default function CollaborationPage(props: CollaborationProps) { } setIsLoadingSubmission(true); sendSubmittingStateToMatchedUser(true); - const data = await ExecuteVisibleAndHiddenTestsAndSubmit( - questionDocRefId, - { - code: code, - language: selectedLanguage, - user: currentUser ?? "", - matchedUser: matchedUser ?? "", - matchedTopics: matchedTopics ?? [], - title: questionTitle ?? "", - questionDifficulty: complexity ?? "", - questionTopics: categories, - } - ); + const data = await ExecuteVisibleAndHiddenTestsAndSubmit(questionDocRefId, { + code: code, + language: selectedLanguage, + user: currentUser ?? "", + matchedUser: matchedUser ?? "", + matchedTopics: matchedTopics ?? [], + title: questionTitle ?? "", + questionDifficulty: complexity ?? "", + questionTopics: categories, + }); setVisibleTestCases(data.visibleTestResults); setSubmissionHiddenTestResultsAndStatus({ hiddenTestResults: data.hiddenTestResults, @@ -254,7 +265,7 @@ export default function CollaborationPage(props: CollaborationProps) { successMessage("Code saved successfully!"); setIsLoadingSubmission(false); sendSubmittingStateToMatchedUser(false); - } + }; const handleCodeChange = (code: string) => { setCode(code); @@ -317,9 +328,7 @@ export default function CollaborationPage(props: CollaborationProps) { label: ( Case {index + 1} @@ -335,21 +344,20 @@ export default function CollaborationPage(props: CollaborationProps) { {isTestResult(item) && (
- {item.passed ? "Passed" : "Failed"}
- Actual Output: {item.actual} + Actual Output:{" "} + {item.actual}
{item.error && ( <> Error: -
- {item.error} -
+
{item.error}
)}
@@ -487,31 +495,50 @@ export default function CollaborationPage(props: CollaborationProps) { )}
- - Session Status: {submissionHiddenTestResultsAndStatus ? submissionHiddenTestResultsAndStatus.status : "Not Attempted"} + Session Status:{" "} + {submissionHiddenTestResultsAndStatus + ? submissionHiddenTestResultsAndStatus.status + : "Not Attempted"}
{submissionHiddenTestResultsAndStatus && ( - Passed {submissionHiddenTestResultsAndStatus.hiddenTestResults.passed} / {submissionHiddenTestResultsAndStatus.hiddenTestResults.total} hidden test cases + Passed{" "} + { + submissionHiddenTestResultsAndStatus.hiddenTestResults + .passed + }{" "} + /{" "} + { + submissionHiddenTestResultsAndStatus.hiddenTestResults + .total + }{" "} + hidden test cases )}
diff --git a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx index a7c8051bcf..d32f67612d 100644 --- a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx +++ b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx @@ -15,8 +15,8 @@ import * as Y from "yjs"; import { yCollab } from "y-codemirror.next"; import { WebrtcProvider } from "y-webrtc"; import { EditorView, basicSetup } from "codemirror"; -import { keymap } from "@codemirror/view" -import { indentWithTab } from "@codemirror/commands" +import { keymap } from "@codemirror/view"; +import { indentWithTab } from "@codemirror/commands"; import { EditorState, Compartment } from "@codemirror/state"; import { javascript, javascriptLanguage } from "@codemirror/lang-javascript"; import { python, pythonLanguage } from "@codemirror/lang-python"; @@ -68,15 +68,15 @@ interface Awareness { executionResultsState: { executionResults: ExecutionResults; id: number; - } + }; executingState: { executing: boolean; id: number; - } + }; submittingState: { submitting: boolean; id: number; - } + }; } export const usercolors = [ @@ -111,8 +111,7 @@ const CollaborativeEditor = forwardRef( props.onCodeChange(update.state.doc.toString()); } }); - - + // Referenced: https://codemirror.net/examples/config/#dynamic-configuration // const autoLanguage = EditorState.transactionExtender.of((tr) => { // if (!tr.docChanged) return null; @@ -196,10 +195,10 @@ const CollaborativeEditor = forwardRef( }); }; - let latestExecutionId: number = (new Date(0)).getTime(); - let latestSubmissionId: number = (new Date(0)).getTime(); - let latestExecutingId: number = (new Date(0)).getTime(); - let latestSubmittingId: number = (new Date(0)).getTime(); + let latestExecutionId: number = new Date(0).getTime(); + let latestSubmissionId: number = new Date(0).getTime(); + let latestExecutingId: number = new Date(0).getTime(); + let latestSubmittingId: number = new Date(0).getTime(); useImperativeHandle(ref, () => ({ endSession: () => { @@ -311,12 +310,14 @@ const CollaborativeEditor = forwardRef( .get(clientID) as Awareness; if ( - state && + state && state.submissionResultsState && state.submissionResultsState.id !== latestSubmissionId ) { latestSubmissionId = state.submissionResultsState.id; - props.updateSubmissionResults(state.submissionResultsState.submissionResults); + props.updateSubmissionResults( + state.submissionResultsState.submissionResults + ); messageApi.open({ type: "success", content: `${ @@ -326,12 +327,14 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.executionResultsState && + state && + state.executionResultsState && state.executionResultsState.id !== latestExecutionId ) { latestExecutionId = state.executionResultsState.id; - props.updateExecutionResults(state.executionResultsState.executionResults); + props.updateExecutionResults( + state.executionResultsState.executionResults + ); messageApi.open({ type: "success", content: `${ @@ -341,8 +344,8 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.executingState && + state && + state.executingState && state.executingState.id !== latestExecutingId ) { latestExecutingId = state.executingState.id; @@ -358,8 +361,8 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.submittingState && + state && + state.submittingState && state.submittingState.id !== latestSubmittingId ) { latestSubmittingId = state.submittingState.id; @@ -367,9 +370,7 @@ const CollaborativeEditor = forwardRef( if (state.submittingState.submitting) { messageApi.open({ type: "info", - content: `${ - props.matchedUser ?? "Peer" - } is saving code...`, + content: `${props.matchedUser ?? "Peer"} is saving code...`, }); } } diff --git a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx index 36fdd948e2..4e4f6acf94 100644 --- a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx +++ b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx @@ -11,11 +11,8 @@ import { } from "@ant-design/icons"; const VideoPanel = () => { - const matchId = localStorage.getItem("collabId")?.toString() ?? ""; - const currentUsername = localStorage.getItem("user")?.toString(); - const matchedUsername = localStorage.getItem("matchedUser")?.toString(); - const currentId = currentUsername + "-" + matchId ?? ""; - const partnerId = matchedUsername + "-" + matchId ?? ""; + const [currentId, setCurrentId] = useState(); + const [partnerId, setPartnerId] = useState(); const remoteVideoRef = useRef(null); const currentUserVideoRef = useRef(null); @@ -30,6 +27,15 @@ const VideoPanel = () => { const [muteOn, setMuteOn] = useState(false); const [isCalling, setIsCalling] = useState(false); + useEffect(() => { + const matchId = localStorage.getItem("collabId")?.toString() ?? ""; + const currentUsername = localStorage.getItem("user")?.toString(); + const matchedUsername = localStorage.getItem("matchedUser")?.toString(); + + setCurrentId(currentUsername + "-" + (matchId ?? "")); + setPartnerId(matchedUsername + "-" + (matchId ?? "")); + }, []); + const handleCall = () => { navigator.mediaDevices .getUserMedia({ @@ -120,7 +126,7 @@ const VideoPanel = () => { } }; } - }, []); + }, [currentId]); // When remote peer initiates end call, we set isCalling to false useEffect(() => { From fdce97ae834ba27204624fa648695b31175c4645 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Fri, 8 Nov 2024 14:17:53 +0800 Subject: [PATCH 2/2] fix(videoPanel): :bug: default for undefined partnerId Include a check to validate if partnerID is not undefined --- apps/frontend/src/components/VideoPanel/VideoPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx index 4e4f6acf94..2db62642e0 100644 --- a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx +++ b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx @@ -43,7 +43,7 @@ const VideoPanel = () => { audio: true, }) .then((stream) => { - if (peerInstance) { + if (peerInstance && partnerId) { const call = peerInstance?.call(partnerId, stream); setCallInstance(call); setIsCalling(true); // Set isCalling as true since it is the initiator