Skip to content

Commit

Permalink
Merge pull request #216 from CS3219-AY2425S1/fix-refresh-matchmaking
Browse files Browse the repository at this point in the history
fix: allow user to instantly queue after refreshing while in queue
  • Loading branch information
shishirbychapur authored Nov 13, 2024
2 parents 0dc80cd + f90a59e commit 0e40b12
Show file tree
Hide file tree
Showing 19 changed files with 513 additions and 275 deletions.
13 changes: 5 additions & 8 deletions backend/matching-service/src/services/ws.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import url from 'url'
import WebSocket, { Server as WebSocketServer } from 'ws'
import loggerUtil from '../common/logger.util'
import { addUserToMatchmaking, removeUserFromMatchingQueue } from '../controllers/matching.controller'
import mqConnection from './rabbitmq.service'

export class WebSocketConnection {
private wss: WebSocketServer
Expand All @@ -14,6 +15,7 @@ export class WebSocketConnection {
this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
const query = url.parse(req.url, true).query
const websocketId = query.id as string
const userId = query.userId as string

if (!websocketId) {
ws.close(1008, 'Missing userId')
Expand All @@ -22,15 +24,9 @@ export class WebSocketConnection {

this.clients.set(websocketId, ws)

// // Close connection after 2 minutes automatically
// const closeAfterTimeout = setTimeout(() => {
// ws.close(1000, 'Connection closed by server after 2 minutes')
// }, 120000)

ws.on('message', (message: string) => this.handleMessage(message, websocketId))
ws.on('close', () => {
// clearTimeout(closeAfterTimeout)
this.handleClose(websocketId)
this.handleClose(websocketId, userId)
})
})
}
Expand Down Expand Up @@ -61,9 +57,10 @@ export class WebSocketConnection {
}

// Handle WebSocket close event
private handleClose(websocketId: string): void {
private handleClose(websocketId: string, userId: string): void {
loggerUtil.info(`User ${websocketId} disconnected`)
this.clients.delete(websocketId)
mqConnection.cancelUser(websocketId, userId)
}

// Send a message to a specific user by websocketId
Expand Down
85 changes: 85 additions & 0 deletions frontend/components/code/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ConfirmDialog from '../customs/confirm-dialog'
import UserAvatar from '../customs/custom-avatar'
import { LongTextSkeleton } from '../customs/custom-loader'
import { Button } from '../ui/button'
import { EndIcon, PlayIcon } from '@/assets/icons'
import { Cross1Icon } from '@radix-ui/react-icons'

export const CodeActions = ({
isLoading,
isViewOnly,
handleRunTests,
isCodeRunning,
isOtherUserOnline,
handleEndSession,
username,
isDialogOpen,
setIsDialogOpen,
handleEndSessionConfirmation,
}: {
isLoading: boolean
isViewOnly: boolean
handleEndSession: () => void
handleRunTests: () => void
isCodeRunning: boolean
isOtherUserOnline: boolean
username: string | undefined
isDialogOpen: boolean
setIsDialogOpen: (isOpen: boolean) => void
handleEndSessionConfirmation: () => void
}) => {
const renderCloseButton = () => {
return isViewOnly ? (
<>
<Cross1Icon className="mr-2" />
Close
</>
) : (
<>
<EndIcon fill="white" className="mr-2" />
End Session
</>
)
}

if (isLoading) {
return <LongTextSkeleton />
}

return (
<div id="control-panel" className="flex justify-between">
<div className="flex gap-3">
{!isViewOnly && (
<Button variant={'primary'} onClick={handleRunTests} disabled={isCodeRunning}>
{isCodeRunning ? (
'Executing...'
) : (
<>
{' '}
<PlayIcon fill="white" height="18px" width="18px" className="mr-2" />
Run test
</>
)}
</Button>
)}
</div>
<div className="flex flex-row items-center">
{!isViewOnly && <UserAvatar username={username ?? ''} isOnline={isOtherUserOnline} />}
<Button className="bg-red hover:bg-red-dark" onClick={handleEndSession}>
{renderCloseButton()}
</Button>
<ConfirmDialog
showCancelButton
dialogData={{
title: 'Warning!',
content:
'Are you sure you want to end the session? This will permanently end the session for both you and the other participant.',
isOpen: isDialogOpen,
}}
closeHandler={() => setIsDialogOpen(false)}
confirmHandler={handleEndSessionConfirmation}
/>
</div>
</div>
)
}
119 changes: 119 additions & 0 deletions frontend/components/code/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use client'

import { FC, useEffect, useRef, useState } from 'react'
import { useSession } from 'next-auth/react'
import { ChatModel } from '@repo/collaboration-types'
import { Button } from '@/components/ui/button'
import Image from 'next/image'
import { ScrollArea } from '@/components/ui/scroll-area'

const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase()
}

const Chat: FC<{
chatData: ChatModel[]
isViewOnly: boolean
handleSendMessage: (msg: string) => void
}> = ({ chatData, isViewOnly, handleSendMessage }) => {
const chatEndRef = useRef<HTMLDivElement | null>(null)
const { data: session } = useSession()
const [value, setValue] = useState('')
const [isChatOpen, setIsChatOpen] = useState(true)

const getChatBubbleFormat = (currUser: string, type: 'label' | 'text') => {
let format = ''
if (currUser === session?.user.username) {
format = 'items-end ml-5'
if (type === 'text') {
format += ' bg-theme-600 rounded-xl text-white'
}
} else {
format = 'items-start text-left mr-5'
if (type === 'text') {
format += ' bg-slate-100 rounded-xl p-2 text-slate-900'
}
}
return format
}

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') {
handleSendMessage(e.currentTarget.value)
setValue('')
e.currentTarget.value = ''
}
}

const toggleChat = () => {
setIsChatOpen(!isChatOpen)
}

useEffect(() => {
if (chatEndRef.current) {
chatEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [chatData])

return (
<>
<div className="border-2 rounded-lg border-slate-100 mt-4 max-h-twoFifthScreen flex flex-col">
<div className="flex items-center justify-between border-b-[1px] pl-3">
<h3 className="text-lg font-medium">Chat</h3>
<Button variant="iconNoBorder" size="icon" onClick={toggleChat}>
<Image
src={`/icons/${isChatOpen ? 'minimise' : 'maximise'}.svg`}
alt="Minimise chat"
width={20}
height={20}
/>
</Button>
</div>

{isChatOpen && (
<ScrollArea className="overflow-y-auto p-3">
{!!chatData?.length &&
Object.values(chatData).map((chat, index) => (
<div
key={index}
className={`flex flex-col gap-1 mb-5 ${getChatBubbleFormat(chat.senderId, 'label')}`}
>
<div className="flex items-center gap-2">
<h4 className="text-xs font-medium">{chat.senderId}</h4>
<span className="text-xs text-slate-400">
{formatTimestamp(chat.createdAt.toString())}
</span>
</div>
<div
className={`text-sm py-2 px-3 text-balance break-words w-full ${getChatBubbleFormat(chat.senderId, 'text')}`}
>
{chat.message}
</div>
</div>
))}
{(!chatData || !chatData?.length) && (
<p className="w-full text-center text-gray-400 text-sm my-1">No chat history</p>
)}
<div ref={chatEndRef}></div>
{!isViewOnly && (
<div className="m-3 mt-0 px-3 py-1 border-[1px] rounded-xl text-sm">
<input
type="text"
className="w-full bg-transparent border-none focus:outline-none"
placeholder="Send a message..."
onKeyDown={handleKeyDown}
value={value}
onChange={(e) => setValue(e.target.value)}
readOnly={isViewOnly}
/>
</div>
)}
</ScrollArea>
)}
</div>
</>
)
}

export default Chat
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { userColor } from '@/util/cursor-colors'
import { oneDark } from '@codemirror/theme-one-dark'
import { javascript } from '@codemirror/lang-javascript'
import { indentWithTab } from '@codemirror/commands'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'

interface IProps {
roomId: string
Expand Down Expand Up @@ -93,15 +94,11 @@ const CodeMirrorEditor = forwardRef(({ roomId, language }: IProps, ref) => {
}, [editorContainerRef, ydoc, ytext, session])

return (
<div
ref={editorContainerRef}
style={{
height: '400px',
overflow: 'scroll',
border: '1px solid lightgray',
backgroundColor: '#282c34',
}}
/>
<ScrollArea className="h-80 rounded-b-xl bg-[#282c34]">
<div ref={editorContainerRef} />
<ScrollBar orientation="vertical" />
<ScrollBar orientation="horizontal" />
</ScrollArea>
)
})

Expand Down
59 changes: 59 additions & 0 deletions frontend/components/code/question.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { LargeTextSkeleton, TextSkeleton } from '@/components/customs/custom-loader'
import { DifficultyLabel } from '@/components/customs/difficulty-label'
import CustomLabel from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { capitalizeFirstLowerRest } from '@/util/string-modification'
import { convertSortedComplexityToComplexity } from '@repo/question-types'
import { Category, Complexity } from '@repo/user-types'

const formatQuestionCategories = (cat: Category[]) => {
return cat.map((c) => capitalizeFirstLowerRest(c)).join(', ')
}

export const CodeQuestion = ({
loading,
title,
complexity,
categories,
description,
}: {
loading: boolean
title: string
complexity: Complexity
categories: Category[]
description: string
}) => {
if (loading) {
return (
<div id="question-data" className="flex-grow border-2 rounded-lg border-slate-100 mt-2 py-2 px-3">
<h3 className="text-lg font-medium">
<TextSkeleton />
</h3>
<div className="flex gap-3 my-2 text-sm">
<TextSkeleton />
<TextSkeleton />
</div>
<div className="mt-6">
<LargeTextSkeleton />
</div>
</div>
)
}
return (
<ScrollArea
id="question-data"
className="flex-grow border-2 rounded-lg border-slate-100 mt-2 py-2 px-3 overflow-y-auto"
>
<h3 className="text-lg font-medium">{title}</h3>
<div className="flex gap-3 my-2 text-sm">
<DifficultyLabel complexity={convertSortedComplexityToComplexity(complexity)} />
<CustomLabel
title={formatQuestionCategories(categories ?? [])}
textColor="text-theme"
bgColor="bg-theme-100"
/>
</div>
<div className="mt-6 whitespace-pre-wrap">{description}</div>
</ScrollArea>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { languages } from '@codemirror/language-data'
import { oneDark } from '@codemirror/theme-one-dark'
import { javascript } from '@codemirror/lang-javascript'
import { indentWithTab } from '@codemirror/commands'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'

interface IProps {
language: string
Expand Down Expand Up @@ -60,15 +61,11 @@ const ReadOnlyCodeMirrorEditor = ({ language, code }: IProps) => {
}, [editorContainerRef, code])

return (
<div
ref={editorContainerRef}
style={{
height: '400px',
overflow: 'scroll',
border: '1px solid lightgray',
backgroundColor: '#282c34',
}}
/>
<ScrollArea className="h-80 rounded-b-xl bg-[#282c34]">
<div ref={editorContainerRef} />
<ScrollBar orientation="vertical" />
<ScrollBar orientation="horizontal" />
</ScrollArea>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { ResultModel } from '@repo/collaboration-types'
import { LargeTextSkeleton } from '../customs/custom-loader'

export default function TestResult({
result,
expectedOutput,
isLoading,
}: {
result?: ResultModel | undefined
expectedOutput: string
isLoading: boolean
}) {
if (isLoading) return <LargeTextSkeleton />

if (!result) return <div className="text-sm text-slate-400">No test results yet</div>

if (!result.status) {
Expand All @@ -18,8 +23,6 @@ export default function TestResult({
)
}

const { id, description } = result.status

return (
<div className="w-full">
<div className="w-full">
Expand Down
Loading

0 comments on commit 0e40b12

Please sign in to comment.