diff --git a/collaboration-service/src/controllers/collaborationController.ts b/collaboration-service/src/controllers/collaborationController.ts index 3001aaa603..8c4bca8634 100644 --- a/collaboration-service/src/controllers/collaborationController.ts +++ b/collaboration-service/src/controllers/collaborationController.ts @@ -145,7 +145,8 @@ export const createRoom = async (req: Request, res: Response) => { } // "random" algorithm to get a random question from the list of questions - const randQuestion = questions[Math.floor(Math.random() * questions.length)]; + const randQuestion = + questions[Math.floor(Math.random() * questions.length)]; const selectedId = randQuestion.questionId; const roomId = uuidv4(); @@ -180,7 +181,10 @@ export const createRoom = async (req: Request, res: Response) => { createdAt: currTime, selectedQuestionId: selectedId, status: "active", - currentLanguage: "javascript", + userLanguages: { + [userId1]: "javascript", + [userId2]: "javascript", + }, }; await set(roomRef, newRoom); diff --git a/collaboration-service/src/models/room-model.ts b/collaboration-service/src/models/room-model.ts index 0d05ced28d..1f145cadd5 100644 --- a/collaboration-service/src/models/room-model.ts +++ b/collaboration-service/src/models/room-model.ts @@ -10,5 +10,13 @@ export interface Room { createdAt: string; selectedQuestionId: number; status: "active" | "inactive"; - currentLanguage: "javascript" | "python" | "csharp" | "java"; + // currentLanguage: "javascript" | "python" | "csharp" | "java"; + userLanguages: { + [userId: string]: "javascript" | "python" | "csharp" | "java"; + }; + languageChangeRequest?: { + requestedBy: string; + newLanguage: "javascript" | "python" | "csharp" | "java"; + timestamp: number; + }; } diff --git a/frontend-service/components/collab/CodeEditor.tsx b/frontend-service/components/collab/CodeEditor.tsx index f715239067..8eb0c18ad1 100644 --- a/frontend-service/components/collab/CodeEditor.tsx +++ b/frontend-service/components/collab/CodeEditor.tsx @@ -15,7 +15,7 @@ import { useToast, } from '@chakra-ui/react'; import { FIREBASE_DB } from '../../FirebaseConfig'; -import { ref, onValue, set, get } from 'firebase/database'; +import { ref, onValue, set, get, child } from 'firebase/database'; import axios from 'axios'; import QuestionSideBar from './QuestionSidebar'; import { useNavigate } from 'react-router-dom'; @@ -41,8 +41,16 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { // Code editor states const [code, setCode] = useState('//Start writing your code here..'); const [codeLanguage, setCodeLanguage] = useState('javascript'); + + const [pendingCodeLanguage, setPendingCodeLanguage] = useState(null); + const [leaveRoomMessage, setLeaveRoomMessage] = useState(null); + const [question, setQuestion] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); // New state + const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'typing' | null>(null); const [isReadOnly, setIsReadOnly] = useState(false); + const [pendingChangeRequest, setPendingChangeRequest] = useState(false); // const [question, setQuestion] = useState(null); // Room states @@ -60,7 +68,13 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { // Firebase references const usersRef = ref(FIREBASE_DB, `rooms/${roomId}/users`); const codeRef = ref(FIREBASE_DB, `rooms/${roomId}/code`); + + const userLanguagesRef = ref(FIREBASE_DB, `rooms/${roomId}/userLanguages`); + const languageChangeRequestRef = ref(FIREBASE_DB, `rooms/${roomId}/languageChangeRequest`); + + const languageRef = ref(FIREBASE_DB, `rooms/${roomId}/currentLanguage`); + const cancelRef = useRef(null); const monacoEditorRef = useRef(null); @@ -125,30 +139,25 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { setupSyntaxHighlighting().catch(console.error); }, []); - // Setup code editor useEffect(() => { - const unsubscribe = onValue(codeRef, (snapshot) => { - const updatedCode = snapshot.val() - if (updatedCode !== null && updatedCode !== code) { - setCode(updatedCode) - if (updatedCode !== null && updatedCode[codeLanguage]) { - setCode(updatedCode[codeLanguage]) - } else { - setCode(languageType[codeLanguage]) // Set to default if no snippet is saved - } - } - }) - const unsubscribeLanguage = onValue(languageRef, (snapshot) => { - const savedLanguage = snapshot.val() - if (savedLanguage) { - setCodeLanguage(savedLanguage) + const loadUserLanguage = async () => { + const userLanguageSnapshot = await get(child(userLanguagesRef, thisUserId)) // userLang references userID with language + const userLanguage = userLanguageSnapshot.val() || 'javascript' // default lang set as javascript + setCodeLanguage(userLanguage) + + // load code snippet + const codeSnippetSnapshot = await get(codeRef) + const codeSnippets = codeSnippetSnapshot.val() + if (codeSnippets && codeSnippets[userLanguage]) { + setCode(codeSnippets[userLanguage]) + } else { + const placeholderCode = languageType[userLanguage] + setCode(placeholderCode) + await set(ref(FIREBASE_DB, `rooms/${roomId}/code/${userLanguage}`), placeholderCode) } - }) - return () => { - unsubscribe() - unsubscribeLanguage() } - }, [code, codeLanguage]) + loadUserLanguage() + }, [thisUserId, roomId]) // Actions based on whether the other user is present useEffect(() => { @@ -199,7 +208,22 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { }); return () => unsubscribe(); - }, [code, codeLanguage, codeRef]); + }, [code, userLanguagesRef, codeRef]); + + // listen for code changes within current language + useEffect(() => { + const codeLanguageRef = ref(FIREBASE_DB, `rooms/${roomId}/code/${codeLanguage}`) + const unsubscribe = onValue(codeLanguageRef, (snapshot) => { + const updatedCode = snapshot.val() + if (updatedCode !== null && updatedCode !== code) { + setCode(updatedCode) + } + }) + + return () => { + unsubscribe() + }; + }, [codeLanguage, code, roomId]) // Handle code changes const handleEditorChange = async (newValue: string | undefined) => { @@ -212,6 +236,21 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { } }; + // listen for changes from other users + useEffect(() => { + const unsubscribe = onValue(languageChangeRequestRef, (snapshot) => { + const req = snapshot.val() + if (req && req.requestedBy !== thisUserId && req.newLanguage !== codeLanguage) { + setPendingCodeLanguage(req.newLanguage) + showLanguageChangeToast(req.newLanguage) + } + }); + + return () => { + unsubscribe() + }; + }, [thisUserId, codeLanguage, toast]) + const formatTimeSinceLastSave = () => { if (!lastSavedTime) return ''; const seconds = Math.floor((new Date().getTime() - lastSavedTime.getTime()) / 1000); @@ -261,7 +300,7 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { const handleLanguageChange = async (event: React.ChangeEvent) => { const selectedLanguage = event.target.value await set(ref(FIREBASE_DB, `rooms/${roomId}/code/${codeLanguage}`), code) - await set(languageRef, selectedLanguage) + await set(child(userLanguagesRef, thisUserId), selectedLanguage) setCodeLanguage(selectedLanguage) const codeSnippetsSnapshot = await get(codeRef) @@ -281,6 +320,95 @@ const CodeEditor: React.FC = ({ roomId, thisUserId }) => { monaco.editor.setModelLanguage(model, selectedLanguage); monacoEditorRef.current.layout(); } + + await set(languageChangeRequestRef, { + requestedBy: thisUserId, + newLanguage: selectedLanguage, + timestamp: Date.now(), + }); + }; + + const showLanguageChangeToast = (newLanguage: string) => { + + if (!newLanguage || pendingChangeRequest) return; + setPendingChangeRequest(true); + + toast({ + position: 'top', + duration: null, + isClosable: true, + render: ({ onClose }) => ( + + + Your partner wants to change the language to {newLanguage}. Do you accept? + + + + + ), + }); + }; + + const confirmLanguageChange = async (newLanguage: string) => { + await set(ref(FIREBASE_DB, `rooms/${roomId}/code/${codeLanguage}`), code); + + await set(child(userLanguagesRef, thisUserId), newLanguage); + setCodeLanguage(newLanguage); + + const codeSnippetsSnapshot = await get(codeRef); + const codeSnippets = codeSnippetsSnapshot.val(); + + if (codeSnippets && codeSnippets[newLanguage]) { + setCode(codeSnippets[newLanguage]); + } else { + const defaultCode = languageType[newLanguage] || '// Start writing your code here...'; + setCode(defaultCode); + await set(ref(FIREBASE_DB, `rooms/${roomId}/code/${newLanguage}`), defaultCode); + } + + if (monacoEditorRef.current) { + const model = monacoEditorRef.current.getModel(); + if (model) { + monaco.editor.setModelLanguage(model, newLanguage); + monacoEditorRef.current.layout(); + } + } + await set(languageChangeRequestRef, { + requestedBy: thisUserId, + newLanguage, + response: 'accepted', + timestamp: Date.now(), + }); + }; + + const declineLanguageChange = () => { }; const handleEditorDidMount: OnMount = (editor) => {