Skip to content

Commit

Permalink
Merge pull request #36 from CS3219-AY2425S1/feature/collab-new-enhance
Browse files Browse the repository at this point in the history
Feature/collab new enhance
  • Loading branch information
techjay-c authored Nov 13, 2024
2 parents 965d91c + 8e8d67e commit 5dea56f
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion collaboration-service/src/models/room-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
176 changes: 152 additions & 24 deletions frontend-service/components/collab/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -41,8 +41,16 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ roomId, thisUserId }) => {
// Code editor states
const [code, setCode] = useState('//Start writing your code here..');
const [codeLanguage, setCodeLanguage] = useState<string>('javascript');

const [pendingCodeLanguage, setPendingCodeLanguage] = useState<string | null>(null);
const [leaveRoomMessage, setLeaveRoomMessage] = useState<string | null>(null);
const [question, setQuestion] = useState<Question | null>(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<Question | null>(null);

// Room states
Expand All @@ -60,7 +68,13 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ 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<monaco.editor.IStandaloneCodeEditor | null>(null);

Expand Down Expand Up @@ -125,30 +139,25 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ 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(() => {
Expand Down Expand Up @@ -199,7 +208,22 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ 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) => {
Expand All @@ -212,6 +236,21 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ 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);
Expand Down Expand Up @@ -261,7 +300,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ roomId, thisUserId }) => {
const handleLanguageChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
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)
Expand All @@ -281,6 +320,95 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ 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 }) => (
<Box
m={3}
color="white"
p={3}
bg="blue.500"
borderRadius="md"
>
<Text fontSize="sm">
Your partner wants to change the language to <b>{newLanguage}</b>. Do you accept?
</Text>
<Button
colorScheme="green"
size="sm"
mr={2}
onClick={() => {
confirmLanguageChange(newLanguage);
setPendingChangeRequest(false);
onClose();
}}
>
Yes
</Button>
<Button
colorScheme="red"
size="sm"
onClick={() => {
declineLanguageChange();
setPendingChangeRequest(false);
onClose();
}}
>
No
</Button>
</Box>
),
});
};

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) => {
Expand Down

0 comments on commit 5dea56f

Please sign in to comment.