diff --git a/backend/code-execution-service/src/utils/code-runner.util.ts b/backend/code-execution-service/src/utils/code-runner.util.ts index 8aee2ff4e1..8995e57ee8 100644 --- a/backend/code-execution-service/src/utils/code-runner.util.ts +++ b/backend/code-execution-service/src/utils/code-runner.util.ts @@ -12,22 +12,23 @@ export async function runCode( const folder = `./code/${Date.now()}`; await fs.mkdir(folder, { recursive: true }); - const sourceFile = `${folder}/solution.${extensions[lang]}`; - const inputFile = `${folder}/input.txt`; + // Create a ran between 1 and 100 + const rand = Math.floor(Math.random() * 100) + 1; + + const sourceFileName = `solution`; + const sourceFile = `${folder}/${sourceFileName}.${extensions[lang]}`; + const inputFile = `${folder}/input-${rand}.txt`; const outputFile = `${folder}/output.txt`; await fs.writeFile(sourceFile, code); await fs.writeFile(inputFile, input); - // Input -> Generates output - // Compare generated output with expected output - let compileCommand = ''; let runCommand = `timeout ${timeout} `; if (lang === 'java') { compileCommand = `javac ${sourceFile}`; - runCommand += `java -cp ${folder} main < ${inputFile}`; + runCommand += `java -cp ${folder} ${sourceFileName} < ${inputFile}`; } else if (lang === 'python3') { runCommand += `python3 ${sourceFile} < ${inputFile}`; } diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 74c4aab4da..50dd31bac8 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -116,6 +116,7 @@ services: - "3005:4000" code-execution-service: + read_only: true build: context: ./code-execution-service target: development diff --git a/backend/gateway-service/src/modules/collaboration/collaboration.message.ts b/backend/gateway-service/src/modules/collaboration/collaboration.message.ts index 0f972bffa3..c521ff9d63 100644 --- a/backend/gateway-service/src/modules/collaboration/collaboration.message.ts +++ b/backend/gateway-service/src/modules/collaboration/collaboration.message.ts @@ -3,6 +3,7 @@ export const SESSION_JOIN = 'sessionJoin'; export const SESSION_LEAVE = 'sessionLeave'; export const SESSION_END = 'sessionEnd'; export const SUBMIT = 'submit'; +export const CHANGE_LANGUAGE = 'changeLanguage'; // Chat messages export const CHAT_SEND_MESSAGE = 'chatSendMessage'; diff --git a/backend/gateway-service/src/modules/collaboration/collaborationws.controller.ts b/backend/gateway-service/src/modules/collaboration/collaborationws.controller.ts index 592abc758d..a4658ffb11 100644 --- a/backend/gateway-service/src/modules/collaboration/collaborationws.controller.ts +++ b/backend/gateway-service/src/modules/collaboration/collaborationws.controller.ts @@ -9,6 +9,7 @@ import { import { Server, Socket } from 'socket.io'; import { JoinCollabSessionRequestDto } from './dto/join-collab-session-request.dto'; import { + CHANGE_LANGUAGE, CHAT_SEND_MESSAGE, SESSION_JOIN, SESSION_LEAVE, @@ -17,6 +18,7 @@ import { import { CHAT_RECIEVE_MESSAGE, EXCEPTION, + LANGUAGE_CHANGED, SESSION_ERROR, SESSION_JOINED, SESSION_LEFT, @@ -45,6 +47,8 @@ export class CollaborationGateway implements OnGatewayDisconnect { private socketUserMap = new Map(); // socketId -> userId private userSocketMap = new Map(); // userId -> socktId + private sessionLanguageMap = new Map(); // sessionId -> language + constructor( @Inject('QUESTION_SERVICE') private questionService: ClientProxy, @Inject('CODE_EXECUTION_SERVICE') private codeExecutionService: ClientProxy, @@ -92,13 +96,24 @@ export class CollaborationGateway implements OnGatewayDisconnect { console.log('sessionjoin and messages retrieved:'); console.log(messages); + const existingLanguage = this.sessionLanguageMap.get(sessionId); + this.sessionLanguageMap.set(sessionId, existingLanguage || 'python3'); + // emit joined event this.server.to(sessionId).emit(SESSION_JOINED, { userId, // the user who recently joined sessionId, messages, // chat messages + language: existingLanguage || 'python3', // default language sessionUserProfiles, // returns the all session member profiles }); + + return { + success: true, + data: { + messages, // chat messages + } + }; } catch (e) { console.log(e); return { @@ -198,10 +213,11 @@ export class CollaborationGateway implements OnGatewayDisconnect { sessionId: string; questionId: string; code: string; + language: string; }, ) { try { - const { userId, sessionId, questionId, code } = payload; + const { userId, sessionId, questionId, code, language } = payload; if (!userId || !sessionId || !code) { client.emit(SESSION_ERROR, 'Invalid submit request payload.'); @@ -229,7 +245,7 @@ export class CollaborationGateway implements OnGatewayDisconnect { { code: code, input: testCase.input, - language: 'python3', + language: language, timeout: 5, // TODO: update this to the correct timeout, default is 5 seconds }, ), @@ -284,6 +300,36 @@ export class CollaborationGateway implements OnGatewayDisconnect { } } + @SubscribeMessage(CHANGE_LANGUAGE) + async handleChangeLanguage( + @ConnectedSocket() client: Socket, + @MessageBody() + payload: { + userId: string; + sessionId: string; + language: string; + }, + ) { + try { + const { userId, sessionId, language } = payload; + + if (!userId || !sessionId || !language) { + client.emit(SESSION_ERROR, 'Invalid change language request payload.'); + return; + } + + this.sessionLanguageMap.set(sessionId, language); + + this.server.to(sessionId).emit(LANGUAGE_CHANGED, { + changedBy: userId, + language, + }); + } catch (error) { + client.emit(EXCEPTION, `Error changing language: ${error.message}`); + return; + } + } + handleDisconnect(@ConnectedSocket() client: Socket) { // When client disconnects from the socket console.log(`User: ${this.socketUserMap.get(client.id)} disconnected`); diff --git a/backend/gateway-service/src/modules/collaboration/collaborationws.event.ts b/backend/gateway-service/src/modules/collaboration/collaborationws.event.ts index 1fd86f2bc0..cfb10647be 100644 --- a/backend/gateway-service/src/modules/collaboration/collaborationws.event.ts +++ b/backend/gateway-service/src/modules/collaboration/collaborationws.event.ts @@ -6,6 +6,7 @@ export const SESSION_ERROR = 'sessionError'; export const EXCEPTION = 'exception'; export const SUBMITTING = 'submitting'; export const SUBMITTED = 'submitted'; +export const LANGUAGE_CHANGED = 'languageChanged'; // Chat events export const CHAT_RECIEVE_MESSAGE = 'chatReceiveMessage'; diff --git a/backend/gateway-service/src/modules/match/match.controller.ts b/backend/gateway-service/src/modules/match/match.controller.ts index 90e24db3f0..05b03f9c20 100644 --- a/backend/gateway-service/src/modules/match/match.controller.ts +++ b/backend/gateway-service/src/modules/match/match.controller.ts @@ -255,12 +255,14 @@ export class MatchGateway implements OnGatewayInit { matchId, matchUserId: user2, matchUsername: user2Details.username, + matchDisplayname: user2Details.displayName, }); this.server.to(user2SocketId).emit(MATCH_FOUND, { message: `You have found a match`, matchId, matchUserId: user1, matchUsername: user1Details.username, + matchDisplayname: user1Details.displayName, }); // Store participants for this matchId diff --git a/frontend/src/app/collaboration/[sessionId]/page.tsx b/frontend/src/app/collaboration/[sessionId]/page.tsx index 8f15d52350..59d11bd751 100644 --- a/frontend/src/app/collaboration/[sessionId]/page.tsx +++ b/frontend/src/app/collaboration/[sessionId]/page.tsx @@ -63,27 +63,27 @@ export default async function Page(props: { params: Params }) { socketUrl={socketUrl} >
- - - - - - - - - - - - - {chatFeature && ( -
- -
- )} -
+ + + + + + + + + + + + + {chatFeature && ( +
+ +
+ )} + ); } diff --git a/frontend/src/app/collaboration/_components/Chat/ChatBottomToolbar.tsx b/frontend/src/app/collaboration/_components/Chat/ChatBottomToolbar.tsx index 4034c80def..a6d3ab7e46 100644 --- a/frontend/src/app/collaboration/_components/Chat/ChatBottomToolbar.tsx +++ b/frontend/src/app/collaboration/_components/Chat/ChatBottomToolbar.tsx @@ -31,11 +31,14 @@ export default function ChatBottomToolbar({ const { handleSubmit, formState, reset } = methods; - const onSubmit = useCallback(async (data: z.infer) => { - if (formState.isSubmitting || data.message.length === 0) return; - handleSendMessage(data.message); - reset({ message: "" }); - }, []); + const onSubmit = useCallback( + async (data: z.infer) => { + if (formState.isSubmitting || data.message.length === 0) return; + handleSendMessage(data.message); + reset({ message: "" }); + }, + [formState.isSubmitting, handleSendMessage, reset] + ); return ( { return getUserProfileDetailByUserId(message.userId); - }, [message]); + }, [message, getUserProfileDetailByUserId]); const isSender = useMemo(() => { return message.userId === userProfile?.id; - }, []); + }, [message.userId, userProfile?.id]); const statusIcon = useMemo(() => { switch (message.status) { diff --git a/frontend/src/app/collaboration/_components/Editor/CollabCodePanel.tsx b/frontend/src/app/collaboration/_components/Editor/CollabCodePanel.tsx index 5b5d509062..9ee8be30b3 100644 --- a/frontend/src/app/collaboration/_components/Editor/CollabCodePanel.tsx +++ b/frontend/src/app/collaboration/_components/Editor/CollabCodePanel.tsx @@ -2,6 +2,7 @@ import { Code, Pencil } from "lucide-react"; import TabPanel, { Tab } from "@/app/collaboration/_components/TabPanel"; import CollaborativeEditorTab from "./CollaborativeEditor/CollaborativeEditorTab"; import CollaborativeWhiteboardTab from "./CollaborativeWhiteboard/CollaborativeWhiteboardTab"; +import LanguageSelector from "./CollaborativeEditor/LanguageSelector"; export function CollabCodePanel() { const tabs: Tab[] = @@ -29,5 +30,11 @@ export function CollabCodePanel() { }, ]; - return ; + return ( + } + /> + ); } diff --git a/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/CollaborativeEditor.tsx b/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/CollaborativeEditor.tsx index 2df9ee235e..4bc773cde6 100644 --- a/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/CollaborativeEditor.tsx +++ b/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/CollaborativeEditor.tsx @@ -14,11 +14,20 @@ import { useSessionContext } from "@/contexts/SessionContext"; import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/LoadingSpinner"; +const defaultEditorValues: { [key: string]: string } = { + python3: "# Start coding here", + java: "public class solution {\n public static void main(String[] args) {\n // Start coding here\n }\n}", +}; + +const editorLanguageMap: { [key: string]: string } = { + python3: "python", + java: "java", +}; + interface CollaborativeEditorProps { sessionId: string; currentUser: UserProfile; socketUrl?: string; - language?: string; themeName?: string; } @@ -26,11 +35,11 @@ export default function CollaborativeEditor({ sessionId, currentUser, socketUrl = "ws://localhost:4001", - language = "python", themeName = "clouds-midnight", }: CollaborativeEditorProps) { - const { codeReview, submitCode, submitting } = useSessionContext(); + const { codeReview, language, submitCode, submitting } = useSessionContext(); const { setCurrentClientCode } = codeReview; + const [editorRef, setEditorRef] = useState(); const [provider, setProvider] = useState(); @@ -40,7 +49,7 @@ export default function CollaborativeEditor({ if (!editorRef) return; const yDoc = new Y.Doc(); - const yTextInstance = yDoc.getText("monaco"); + const yTextInstance = yDoc.getText(`code-${language}`); const yProvider = new WebsocketProvider( `${socketUrl}/yjs?sessionId=${sessionId}&userId=${currentUser.id}`, `c_${sessionId}`, @@ -68,7 +77,14 @@ export default function CollaborativeEditor({ yDoc.destroy(); binding.destroy(); }; - }, [sessionId, currentUser, socketUrl, editorRef, setCurrentClientCode]); + }, [ + sessionId, + currentUser, + socketUrl, + editorRef, + setCurrentClientCode, + language, + ]); const handleEditorOnMount = useCallback( (e: editor.IStandaloneCodeEditor, monaco: Monaco) => { @@ -84,7 +100,7 @@ export default function CollaborativeEditor({ return (
); diff --git a/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/LanguageSelector.tsx b/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/LanguageSelector.tsx new file mode 100644 index 0000000000..5981308345 --- /dev/null +++ b/frontend/src/app/collaboration/_components/Editor/CollaborativeEditor/LanguageSelector.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useSessionContext } from "@/contexts/SessionContext"; +import { cn } from "@/lib/utils"; + +interface LanguageSelectorProps { + className?: string; +} + +export default function LanguageSelector({ className }: LanguageSelectorProps) { + const { language, changeLanguage } = useSessionContext(); + + return ( + + ); +} diff --git a/frontend/src/app/collaboration/_components/TabPanel.tsx b/frontend/src/app/collaboration/_components/TabPanel.tsx index 3cd5e10d19..692b2b24a3 100644 --- a/frontend/src/app/collaboration/_components/TabPanel.tsx +++ b/frontend/src/app/collaboration/_components/TabPanel.tsx @@ -17,6 +17,7 @@ export interface TabPanelProps extends TabsProps { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; + tabChildren?: ReactNode; } export default function TabPanel({ @@ -36,6 +37,7 @@ export default function TabPanel({ >
{ return { value: tab.value, label: tab.label, Icon: tab.Icon }; })} @@ -56,9 +58,10 @@ export default function TabPanel({ interface TabOptionsProps { options: TabButtonProps[]; + tabChildren?: ReactNode; } -function TabOptions({ options }: TabOptionsProps) { +function TabOptions({ options, tabChildren }: TabOptionsProps) { return ( {options.map((option) => ( @@ -69,6 +72,7 @@ function TabOptions({ options }: TabOptionsProps) { Icon={option.Icon} /> ))} + {tabChildren} ); } diff --git a/frontend/src/app/dashboard/_components/FindMatch/ConfirmationDialog.tsx b/frontend/src/app/dashboard/_components/FindMatch/ConfirmationDialog.tsx index e7975625c9..df94dccae7 100644 --- a/frontend/src/app/dashboard/_components/FindMatch/ConfirmationDialog.tsx +++ b/frontend/src/app/dashboard/_components/FindMatch/ConfirmationDialog.tsx @@ -27,7 +27,7 @@ interface ConfirmationDialogProps { export default function ConfirmationDialog({ user }: ConfirmationDialogProps) { const { matchFound, - matchUsername, + matchDisplayname, isAwaitingConfirmation, handleDeclineMatch, handleAcceptMatch, @@ -83,16 +83,21 @@ export default function ConfirmationDialog({ user }: ConfirmationDialogProps) { - {getInitialsFromName(user.displayName)} + {getInitialsFromName(user.displayName.toUpperCase())} + - - - - {matchUsername ? getInitialsFromName(matchUsername) : "?"} - - +
+ + + + {matchDisplayname + ? getInitialsFromName(matchDisplayname.toUpperCase()) + : "?"} + + +
diff --git a/frontend/src/app/dashboard/_components/QuestionsStatsCard.tsx b/frontend/src/app/dashboard/_components/QuestionsStatsCard.tsx index a2ee48be6d..125be0c4f2 100644 --- a/frontend/src/app/dashboard/_components/QuestionsStatsCard.tsx +++ b/frontend/src/app/dashboard/_components/QuestionsStatsCard.tsx @@ -20,7 +20,15 @@ import { Label, Pie, PieChart, ResponsiveContainer } from "recharts"; export function QuestionsStatsCard() { return ( - + {/* */} +
+

30

+
+ Questions + attempted +
+
+
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index b8e544535a..614f20b542 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -33,26 +33,26 @@ const buttonVariants = cva( size: "default", }, } -) +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -) -Button.displayName = "Button" +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx index cc4e0ab2f0..84dfcfbd61 100644 --- a/frontend/src/components/ui/toast.tsx +++ b/frontend/src/components/ui/toast.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -import * as React from "react" -import { Cross2Icon } from "@radix-ui/react-icons" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -16,16 +16,16 @@ const ToastViewport = React.forwardRef< -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-left-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", { variants: { variant: { @@ -38,7 +38,7 @@ const toastVariants = cva( variant: "default", }, } -) +); const Toast = React.forwardRef< React.ElementRef, @@ -51,9 +51,9 @@ const Toast = React.forwardRef< className={cn(toastVariants({ variant }), className)} {...props} /> - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -67,8 +67,8 @@ const ToastAction = React.forwardRef< )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -85,8 +85,8 @@ const ToastClose = React.forwardRef< > -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, @@ -97,8 +97,8 @@ const ToastTitle = React.forwardRef< className={cn("text-sm font-semibold [&+div]:text-xs", className)} {...props} /> -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, @@ -109,12 +109,12 @@ const ToastDescription = React.forwardRef< className={cn("text-sm opacity-90", className)} {...props} /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { type ToastProps, @@ -126,4 +126,4 @@ export { ToastDescription, ToastClose, ToastAction, -} +}; diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx index 171beb46d9..ae96ee99d5 100644 --- a/frontend/src/components/ui/toaster.tsx +++ b/frontend/src/components/ui/toaster.tsx @@ -14,7 +14,7 @@ export function Toaster() { const { toasts } = useToast() return ( - + {toasts.map(function ({ id, title, description, action, ...props }) { return ( diff --git a/frontend/src/contexts/FindMatchContext.tsx b/frontend/src/contexts/FindMatchContext.tsx index 1c711c8ca1..a9e32827a1 100644 --- a/frontend/src/contexts/FindMatchContext.tsx +++ b/frontend/src/contexts/FindMatchContext.tsx @@ -22,6 +22,7 @@ interface FindMatchContextProps { isConnected: boolean; matchId?: string; matchUsername?: string; + matchDisplayname?: string; findingMatch: boolean; matchFound: boolean; isAwaitingConfirmation: boolean; @@ -84,6 +85,10 @@ export function FindMatchProvider({ const [matchUsername, setMatchUsername] = useState(); + const [matchDisplayname, setMatchDisplayname] = useState< + string | undefined + >(); + const matchRequest: MatchRequest = useMemo(() => { return { userId: userId, @@ -155,12 +160,15 @@ export function FindMatchProvider({ ({ matchId, matchUsername, + matchDisplayname, }: { matchId: string; matchUsername: string; + matchDisplayname: string; }) => { setMatchId(matchId); setMatchUsername(matchUsername); + setMatchDisplayname(matchDisplayname); setFindingMatch(false); setMatchFound(true); setIsAwaitingConfirmation(false); @@ -270,6 +278,7 @@ export function FindMatchProvider({ isConnected, matchId, matchUsername, + matchDisplayname, findingMatch, matchFound, isAwaitingConfirmation, diff --git a/frontend/src/contexts/SessionContext.tsx b/frontend/src/contexts/SessionContext.tsx index b5b40656e4..f603be1d1d 100644 --- a/frontend/src/contexts/SessionContext.tsx +++ b/frontend/src/contexts/SessionContext.tsx @@ -7,6 +7,7 @@ import { useMemo, useCallback, useEffect, + // SetStateAction, Dispatch, } from "react"; import { SessionUserProfiles, @@ -16,7 +17,6 @@ import { import { createCodeReview } from "@/services/collaborationService"; import { CodeReview } from "@/types/CodeReview"; import { io } from "socket.io-client"; -import { SessionJoinRequest } from "@/types/SessionInfo"; import { ChatMessage, ChatMessages, @@ -27,9 +27,14 @@ import { import { v4 as uuidv4 } from "uuid"; import { Question } from "@/types/Question"; import { SubmissionResult } from "@/types/TestResult"; +import { LoadingSpinner } from "@/components/LoadingSpinner"; +import { SessionJoinRequest } from "@/types/SessionInfo"; +import { Frown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; interface SessionContextType { - isConnected: boolean; + connectionStatus: "connecting" | "connected" | "failed"; sessionId: string; sessionUserProfiles: SessionUserProfiles; userProfile: UserProfile | null; @@ -38,6 +43,8 @@ interface SessionContextType { handleSendMessage: (message: string) => void; setSessionId: (sessionId: string) => void; setUserProfile: (userProfile: UserProfile) => void; + language: string; + changeLanguage: (language: string) => void; submitCode: () => void; submitting: boolean; submissionResult?: SubmissionResult; @@ -71,7 +78,9 @@ export const SessionProvider: React.FC = ({ question, children, }) => { - const [isConnected, setIsConnected] = useState(false); + const [connectionStatus, setConnectionStatus] = useState< + "connecting" | "connected" | "failed" + >("connecting"); const [sessionId, setSessionId] = useState(initialSessionId); const [sessionUserProfiles, setSessionUserProfiles] = useState([]); @@ -182,13 +191,100 @@ export const SessionProvider: React.FC = ({ } ); }, + [sessionId, socket, userProfile.id] + ); + + const handleJoinSession = useCallback( + (payload: SessionJoinRequest) => { + if (!socket.connected) return; + + socket.emit( + "sessionJoin", + payload, + (ack: { + success: boolean; + data: { messages: ChatMessages }; + error: string | undefined; + }) => { + try { + if (!ack.success) throw new Error(ack.error); + setConnectionStatus("connected"); + const currentMessages = ChatMessagesSchema.parse( + ack.data.messages.map((message: ChatMessage) => ({ + ...message, + status: ChatMessageStatusEnum.enum.sent, + })) + ); + setMessages([...currentMessages]); + } catch (e) { + setConnectionStatus("failed"); + } + } + ); + }, [socket] ); - const sessionJoinRequest: SessionJoinRequest = { - userId: userProfile.id, - sessionId, - }; + const onSessionJoined = useCallback( + ({ + language, + sessionUserProfiles, + }: { + language: string; + sessionUserProfiles: SessionUserProfiles; + }) => { + console.log("sessionJoined occured"); + try { + _setLanguage(language); + + const currentSessionUserProfiles = + SessionUserProfilesSchema.parse(sessionUserProfiles); + setSessionUserProfiles([...currentSessionUserProfiles]); + } catch (e) { + // TODO toast here + console.log(e); + } + }, + [] + ); + + const onChatReceiveMessage = useCallback( + (newMessage: ChatMessage) => { + try { + newMessage["status"] = ChatMessageStatusEnum.enum.sent; + const messageParsed = ChatMessageSchema.parse(newMessage); + + if (messageParsed.userId === userProfile.id) return; + + setMessages((prev) => [...prev, messageParsed]); + } catch (e) { + console.log(e); + } + }, + [userProfile.id] + ); + + const onSessionLeft = useCallback( + ({ + sessionUserProfiles, + }: { + userId: string; + sessionUserProfiles: string; + }) => { + try { + console.log("sessionLeft occured"); + + const currentSessionUserProfiles = + SessionUserProfilesSchema.parse(sessionUserProfiles); + setSessionUserProfiles([...currentSessionUserProfiles]); + + console.log(userProfile); + } catch (e) { + console.error(e); + } + }, + [userProfile] + ); const [submitting, setSubmitting] = useState(false); @@ -196,6 +292,26 @@ export const SessionProvider: React.FC = ({ const [testResultPanel, setTestResultPanel] = useState("test-cases"); + const [language, _setLanguage] = useState("python3"); + + const changeLanguage = useCallback( + (language: string) => { + socket.emit("changeLanguage", { + userId: userProfile.id, + sessionId: sessionId, + language, + }); + }, + [sessionId, socket, userProfile.id] + ); + + const onLanguageChanged = useCallback( + ({ language }: { changedBy: string; language: string }) => { + _setLanguage(language); + }, + [] + ); + const submitCode = useCallback(() => { if (submitting) { return; @@ -210,8 +326,17 @@ export const SessionProvider: React.FC = ({ sessionId: sessionId, questionId: question._id, code: codeReview.currentClientCode, + language: language, }); - }, [socket, userProfile, sessionId, codeReview.currentClientCode]); + }, [ + submitting, + socket, + userProfile.id, + sessionId, + question._id, + codeReview.currentClientCode, + language, + ]); const onSubmitting = useCallback(({}: { message: string }) => { setSubmitting(true); @@ -240,79 +365,47 @@ export const SessionProvider: React.FC = ({ socket.connect(); socket.on("connect", () => { - socket.emit("sessionJoin", sessionJoinRequest); - }); - - socket.on("sessionJoined", ({ userId, messages, sessionUserProfiles }) => { - console.log("sessionJoined occured"); - try { - if (userId === userProfile.id) { - setIsConnected(true); - const currentMessages = ChatMessagesSchema.parse( - messages.map((message: ChatMessage) => ({ - ...message, - status: ChatMessageStatusEnum.enum.sent, - })) - ); - setMessages([...currentMessages]); - } - - const currentSessionUserProfiles = - SessionUserProfilesSchema.parse(sessionUserProfiles); - setSessionUserProfiles([...currentSessionUserProfiles]); - } catch (e) { - console.log(e); - } + handleJoinSession({ + userId: userProfile.id, + sessionId, + }); }); - socket.on( - "sessionLeft", - ({ - sessionUserProfiles, - }: { - userId: string; - sessionUserProfiles: string; - }) => { - try { - console.log("sessionLeft occured"); - - const currentSessionUserProfiles = - SessionUserProfilesSchema.parse(sessionUserProfiles); - setSessionUserProfiles([...currentSessionUserProfiles]); - - console.log(userProfile); - } catch (e) { - console.log(e); - } - } - ); + socket.on("sessionJoined", onSessionJoined); - socket.on("chatReceiveMessage", (data) => { - try { - data["status"] = ChatMessageStatusEnum.enum.sent; - const messageParsed = ChatMessageSchema.parse(data); + socket.on("sessionLeft", onSessionLeft); - if (messageParsed.userId === userProfile.id) return; - - setMessages((prev) => [...prev, messageParsed]); - } catch (e) { - console.log(e); - } - }); + socket.on("chatReceiveMessage", onChatReceiveMessage); socket.on("submitting", onSubmitting); socket.on("submitted", onSubmitted); + socket.on("languageChanged", onLanguageChanged); + return () => { - socket.emit("sessionLeave", sessionJoinRequest); + socket.emit("sessionLeave", { + userId: userProfile.id, + sessionId, + }); socket.removeAllListeners(); socket.disconnect(); }; - }, [socket]); + }, [ + onSubmitted, + onSubmitting, + sessionId, + socket, + userProfile, + handleJoinSession, + onChatReceiveMessage, + onSessionJoined, + onSessionLeft, + onLanguageChanged, + ]); const contextValue: SessionContextType = useMemo( () => ({ - isConnected, + connectionStatus, sessionId, setSessionId, sessionUserProfiles, @@ -322,6 +415,8 @@ export const SessionProvider: React.FC = ({ messages, setMessages, handleSendMessage, + language, + changeLanguage, submitCode, submitting, submissionResult, @@ -335,7 +430,7 @@ export const SessionProvider: React.FC = ({ }, }), [ - isConnected, + connectionStatus, codeReview, sessionId, sessionUserProfiles, @@ -343,6 +438,8 @@ export const SessionProvider: React.FC = ({ userProfile, messages, handleSendMessage, + language, + changeLanguage, submitCode, submitting, submissionResult, @@ -355,7 +452,13 @@ export const SessionProvider: React.FC = ({ return ( - {children} + {connectionStatus === "connected" ? ( + children + ) : connectionStatus === "connecting" ? ( + + ) : ( + + )} ); }; @@ -367,3 +470,36 @@ export const useSessionContext = (): SessionContextType => { } return context; }; + +// Ideally should be extracted as a generic component +function LoadingSessionComponent() { + return ( +
+ +

Joining the collaboration session...

+
+ ); +} + +function LoadingErrorSessionComponent() { + const router = useRouter(); + + return ( +
+ +
+

Something went wrong while joining the session.

+

Please try again or find another match.

+
+
+ + +
+
+ ); +}