From 2cabb223b1365c915d70dd3eb70d5fe65dbe1cac Mon Sep 17 00:00:00 2001 From: Lynn low Date: Tue, 29 Oct 2024 23:57:40 +0800 Subject: [PATCH 1/8] Add socket --- backend/collaboration-service/package.json | 1 + .../collaboration-service/src/chat-server.ts | 64 ++++++ frontend/package.json | 1 + frontend/pages/code/[id]/chat.tsx | 141 +++++++++++++ package-lock.json | 191 +++++++++++++++++- 5 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 backend/collaboration-service/src/chat-server.ts create mode 100644 frontend/pages/code/[id]/chat.tsx diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json index 224d8d44b3..c19fb11ef2 100644 --- a/backend/collaboration-service/package.json +++ b/backend/collaboration-service/package.json @@ -29,6 +29,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "socket.io": "^4.8.1", "winston": "^3.14.2", "y-websocket": "^2.0.4" }, diff --git a/backend/collaboration-service/src/chat-server.ts b/backend/collaboration-service/src/chat-server.ts new file mode 100644 index 0000000000..832133cb49 --- /dev/null +++ b/backend/collaboration-service/src/chat-server.ts @@ -0,0 +1,64 @@ +import express from 'express' +import http from 'http' +import { Server, Socket } from 'socket.io' +import path from 'path' + +const app = express() +const server = http.createServer(app) +const io = new Server(server) + +app.use(express.static(path.join(__dirname, '../public'))) + +export interface IMessage { + text: string + name: string + id: string + socketId: string + roomId: string + image?: string +} + +// room id -> socket[] +let roomUsers: Record> = {} + +io.on('connection', (socket: Socket) => { + io.emit('users_response', roomUsers) + + socket.on('join_room', (roomId) => { + socket.join(roomId) + roomUsers = { + ...roomUsers, + [roomId]: { ...(roomUsers[roomId] ?? {}), ...{ [socket.id]: Socket } }, + } + io.emit('users_response', roomUsers) + }) + + socket.on('send_message', (data: IMessage) => { + Object.values(roomUsers[data.roomId]).forEach((s) => s.emit('receive_message', data)) + }) + + socket.on('disconnect', () => { + for (const [roomId, users] of Object.entries(roomUsers)) { + if (Object.keys(users).includes(socket.id)) { + delete roomUsers[roomId][socket.id] + Object.values(roomUsers[roomId]).forEach((s) => + s.emit('receive_message', { + text: 'A user left the room.', + socketId: 'kurakani', + roomId: roomId, + }) + ) + } + } + io.emit('users_response', roomUsers) + }) +}) +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../public', 'index.html')) +}) + +const PORT = 3001 + +server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`) +}) diff --git a/frontend/package.json b/frontend/package.json index 330bf4ae94..289567a868 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "react-dom": "^18", "react-select": "^5.8.1", "recoil": "^0.7.7", + "socket.io-client": "^4.8.1", "sonner": "^1.5.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx new file mode 100644 index 0000000000..8bad3529ce --- /dev/null +++ b/frontend/pages/code/[id]/chat.tsx @@ -0,0 +1,141 @@ +import { FC, useEffect, useRef, useState } from 'react' + +import { mockChatData, mockUserData } from '@/mock-data' +import { Category } from '@repo/user-types' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/router' +import * as socketIO from 'socket.io-client' + +interface ICollaborator { + name: string + email: string +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface Props {} + +const userData: ICollaborator = mockUserData +const initialChatData = mockChatData + +const formatQuestionCategories = (cat: Category[]) => { + return cat.join(', ') +} + +const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp) + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() +} + +export interface IMessage { + text: string + name: string + id: string + socketId: string + roomId: string + image?: string +} + +// eslint-disable-next-line arrow-body-style +const Chat: FC = () => { + const [chatData, setChatData] = useState<{ [key: string]: IMessage[] }>() + const [socket, setSocket] = useState() + const chatEndRef = useRef(null) + const { data: session } = useSession() + const router = useRouter() + const [value, setValue] = useState('') + const { id: roomId } = router.query + + useEffect(() => { + if (!session) { + router.replace('/') + return + } + const socket = socketIO.connect('ws://localhost:3009') + socket.on('receive_message', (data: IMessage) => { + setChatData((prev) => { + const newMessages = { ...prev } + newMessages[data.roomId] = [...(newMessages[data.roomId] ?? []), data] + return newMessages + }) + }) + setSocket(socket) + }, [router, session]) + + const getChatBubbleFormat = (currUser: ICollaborator, type: 'label' | 'text') => { + let format = '' + if (currUser.email === userData.email) { + format = 'items-end ml-5' + // Add more format based on the type + if (type === 'text') { + format += ' bg-theme-600 rounded-xl text-white' + } + } else { + format = 'items-start text-left mr-5' + // Add more format based on the type + if (type === 'text') { + format += ' bg-slate-100 rounded-xl p-2 text-slate-900' + } + } + + return format + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') { + handleSendMessage(e.currentTarget.value) + e.currentTarget.value = '' + } + } + + const handleSendMessage = (message: string) => { + if (!session || !socket) return + + if (message.trim()) { + socket.emit('send_message', { + text: message, + name: session.user.username, + time: new Date(), + socketId: socket.id, + roomId, + }) + } + setValue('') + } + + return ( + <> +
+ {!!chatData?.length && + Object.values(chatData).map((chat, index) => ( +
+
+

{chat.user.name}

+ {formatTimestamp(chat.timestamp)} +
+
+ {chat.text} +
+
+ ))} +
+
+
+ setValue(e.target.value)} + /> +
+ + ) +} + +export default Chat diff --git a/package-lock.json b/package-lock.json index 1edd48ece0..4268ecfbc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "socket.io": "^4.8.1", "winston": "^3.14.2", "y-websocket": "^2.0.4" }, @@ -290,6 +291,7 @@ "react-dom": "^18", "react-select": "^5.8.1", "recoil": "^0.7.7", + "socket.io-client": "^4.8.1", "sonner": "^1.5.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", @@ -3818,6 +3820,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@swc/counter": { "version": "0.1.3", "license": "Apache-2.0" @@ -4052,6 +4059,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "dev": true, @@ -4059,7 +4071,6 @@ }, "node_modules/@types/cors": { "version": "2.8.17", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4238,7 +4249,6 @@ }, "node_modules/@types/node": { "version": "22.5.5", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -5518,6 +5528,14 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "hasInstallScript": true, @@ -6886,6 +6904,94 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "dev": true, @@ -13242,6 +13348,78 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sonner": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", @@ -14380,7 +14558,6 @@ }, "node_modules/undici-types": { "version": "6.19.8", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -14939,6 +15116,14 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", From d022d4ba554a5a723fe9b87cdced6247521d7ebb Mon Sep 17 00:00:00 2001 From: Lynn low Date: Wed, 30 Oct 2024 00:07:26 +0800 Subject: [PATCH 2/8] Chat --- .../collaboration-service/src/chat-server.ts | 20 ++-- frontend/pages/code/[id]/chat.tsx | 52 +++++----- frontend/pages/code/[id]/index.tsx | 97 +------------------ 3 files changed, 38 insertions(+), 131 deletions(-) diff --git a/backend/collaboration-service/src/chat-server.ts b/backend/collaboration-service/src/chat-server.ts index 832133cb49..fabee25e6e 100644 --- a/backend/collaboration-service/src/chat-server.ts +++ b/backend/collaboration-service/src/chat-server.ts @@ -12,10 +12,10 @@ app.use(express.static(path.join(__dirname, '../public'))) export interface IMessage { text: string name: string - id: string + email: string socketId: string roomId: string - image?: string + time: string } // room id -> socket[] @@ -41,13 +41,15 @@ io.on('connection', (socket: Socket) => { for (const [roomId, users] of Object.entries(roomUsers)) { if (Object.keys(users).includes(socket.id)) { delete roomUsers[roomId][socket.id] - Object.values(roomUsers[roomId]).forEach((s) => - s.emit('receive_message', { - text: 'A user left the room.', - socketId: 'kurakani', - roomId: roomId, - }) - ) + const msg: IMessage = { + text: 'A user left the room.', + socketId: 'kurakani', + roomId: roomId, + time: new Date().toString(), + name: '', + email: '', + } + Object.values(roomUsers[roomId]).forEach((s) => s.emit('receive_message', msg)) } } io.emit('users_response', roomUsers) diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index 8bad3529ce..6431ee6238 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -1,7 +1,6 @@ -import { FC, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' -import { mockChatData, mockUserData } from '@/mock-data' -import { Category } from '@repo/user-types' +import { mockUserData } from '@/mock-data' import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import * as socketIO from 'socket.io-client' @@ -11,15 +10,7 @@ interface ICollaborator { email: string } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface Props {} - const userData: ICollaborator = mockUserData -const initialChatData = mockChatData - -const formatQuestionCategories = (cat: Category[]) => { - return cat.join(', ') -} const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp) @@ -29,15 +20,14 @@ const formatTimestamp = (timestamp: string) => { export interface IMessage { text: string name: string - id: string + email: string socketId: string roomId: string - image?: string + time: string } -// eslint-disable-next-line arrow-body-style -const Chat: FC = () => { - const [chatData, setChatData] = useState<{ [key: string]: IMessage[] }>() +const Chat = () => { + const [chatData, setChatData] = useState() const [socket, setSocket] = useState() const chatEndRef = useRef(null) const { data: session } = useSession() @@ -53,9 +43,7 @@ const Chat: FC = () => { const socket = socketIO.connect('ws://localhost:3009') socket.on('receive_message', (data: IMessage) => { setChatData((prev) => { - const newMessages = { ...prev } - newMessages[data.roomId] = [...(newMessages[data.roomId] ?? []), data] - return newMessages + return [...(prev ?? []), data] }) }) setSocket(socket) @@ -91,17 +79,25 @@ const Chat: FC = () => { if (!session || !socket) return if (message.trim()) { - socket.emit('send_message', { + const msg: IMessage = { text: message, name: session.user.username, - time: new Date(), - socketId: socket.id, - roomId, - }) + time: new Date().toString(), + socketId: socket.id || '', + roomId: roomId as string, + email: session.user.email, + } + socket.emit('send_message', msg) } setValue('') } + useEffect(() => { + if (chatEndRef.current) { + chatEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [chatData]) + return ( <>
@@ -109,14 +105,14 @@ const Chat: FC = () => { Object.values(chatData).map((chat, index) => (
-

{chat.user.name}

- {formatTimestamp(chat.timestamp)} +

{chat.name}

+ {formatTimestamp(chat.time)}
{chat.text}
diff --git a/frontend/pages/code/[id]/index.tsx b/frontend/pages/code/[id]/index.tsx index 890040c41d..9572e94251 100644 --- a/frontend/pages/code/[id]/index.tsx +++ b/frontend/pages/code/[id]/index.tsx @@ -11,7 +11,7 @@ import 'ace-builds/src-noconflict/ext-language_tools' import { EndIcon, PlayIcon, SubmitIcon } from '@/assets/icons' import { ITestcase } from '@/types' -import { mockChatData, mockTestCaseData, mockUserData } from '@/mock-data' +import { mockTestCaseData } from '@/mock-data' import { useEffect, useRef, useState } from 'react' import AceEditor from 'react-ace' @@ -31,49 +31,17 @@ import { useSession } from 'next-auth/react' import { getMatchDetails } from '@/services/matching-service-api' import { convertSortedComplexityToComplexity } from '@repo/question-types' import { ReloadIcon } from '@radix-ui/react-icons' -import { toast } from 'sonner' +import Chat from './chat' -interface ICollaborator { - name: string - email: string -} - -const userData: ICollaborator = mockUserData -const initialChatData = mockChatData const testCasesData: ITestcase[] = mockTestCaseData const formatQuestionCategories = (cat: Category[]) => { return cat.join(', ') } -const formatTimestamp = (timestamp: string) => { - const date = new Date(timestamp) - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() -} - -const getChatBubbleFormat = (currUser: ICollaborator, type: 'label' | 'text') => { - let format = '' - if (currUser.email === userData.email) { - format = 'items-end ml-5' - // Add more format based on the type - if (type === 'text') { - format += ' bg-theme-600 rounded-xl text-white' - } - } else { - format = 'items-start text-left mr-5' - // Add more format based on the type - if (type === 'text') { - format += ' bg-slate-100 rounded-xl p-2 text-slate-900' - } - } - - return format -} - export default function Code() { const router = useRouter() const [isChatOpen, setIsChatOpen] = useState(true) - const [chatData, setChatData] = useState(initialChatData) const { id } = router.query const [editorLanguage, setEditorLanguage] = useState('javascript') const testTabs = ['Testcases', 'Test Results'] @@ -98,37 +66,10 @@ export default function Code() { retrieveMatchDetails() }, []) - // Ref for autoscroll the last chat message - const chatEndRef = useRef(null) - const toggleChat = () => { setIsChatOpen(!isChatOpen) } - // TODO: create message service to handle messages - const sendMessage = (message: string) => { - const newMessage = { - user: userData, - message, - timestamp: new Date().toISOString(), - } - setChatData([...chatData, newMessage]) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') { - sendMessage(e.currentTarget.value) - e.currentTarget.value = '' - } - } - - // Scroll to the bottom of the chat box when new messages are added - useEffect(() => { - if (chatEndRef.current) { - chatEndRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [chatData]) - const handleLanguageModeSelect = (value: string) => { console.log('Hey', value) setEditorLanguage(value) @@ -203,39 +144,7 @@ export default function Code() { />
- {isChatOpen && ( - <> -
- {chatData.map((chat, index) => ( -
-
-

{chat.user.name}

- - {formatTimestamp(chat.timestamp)} - -
-
- {chat.message} -
-
- ))} -
-
-
- -
- - )} + {isChatOpen && }
From 555f7e97f78dd01db3c6936b2ac3be19297c5a57 Mon Sep 17 00:00:00 2001 From: Low Han Date: Fri, 1 Nov 2024 00:01:50 +0800 Subject: [PATCH 3/8] Add chat --- backend/collaboration-service/package.json | 3 +- .../collaboration-service/src/chat-server.ts | 57 ++++++++++++++++--- frontend/pages/code/[id]/chat.tsx | 18 +++--- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json index c19fb11ef2..52753816e7 100644 --- a/backend/collaboration-service/package.json +++ b/backend/collaboration-service/package.json @@ -9,7 +9,8 @@ "test": "jest", "lint": "eslint . --fix --no-error-on-unmatched-pattern && prettier --write --ignore-unknown .", "start": "npm run build && node dist/server.js", - "dev": "nodemon src/server.ts" + "dev": "nodemon src/server.ts", + "chat": "nodemon src/chat-server.ts" }, "license": "MIT", "dependencies": { diff --git a/backend/collaboration-service/src/chat-server.ts b/backend/collaboration-service/src/chat-server.ts index fabee25e6e..f5998eb040 100644 --- a/backend/collaboration-service/src/chat-server.ts +++ b/backend/collaboration-service/src/chat-server.ts @@ -1,13 +1,41 @@ -import express from 'express' +import express, { NextFunction, Request, Response } from 'express' import http from 'http' import { Server, Socket } from 'socket.io' +import cors from 'cors' +import helmet from 'helmet' import path from 'path' const app = express() -const server = http.createServer(app) -const io = new Server(server) app.use(express.static(path.join(__dirname, '../public'))) +app.use(express.urlencoded({ extended: true })) +app.use(express.json()) +app.use(cors()) // config cors so that front-end can use +app.options('*', cors()) +app.use(helmet()) +app.use((request: Request, response: Response, next: NextFunction) => { + response.header('Access-Control-Allow-Origin', '*') // "*" -> Allow all links to access + + response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') + + // Browsers usually send this before PUT or POST Requests + if (request.method === 'OPTIONS') { + response.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT, PATCH') + response.status(200).send() + return + } + + // Continue Route Processing + next() +}) + +const server = http.createServer(app) +const io = new Server(server, { + cors: { + origin: '*', + methods: '*', + }, +}) export interface IMessage { text: string @@ -19,22 +47,27 @@ export interface IMessage { } // room id -> socket[] -let roomUsers: Record> = {} - +let roomUsers: Record = {} +const sockets: Record = {} io.on('connection', (socket: Socket) => { + sockets[socket.id] = socket io.emit('users_response', roomUsers) socket.on('join_room', (roomId) => { socket.join(roomId) roomUsers = { ...roomUsers, - [roomId]: { ...(roomUsers[roomId] ?? {}), ...{ [socket.id]: Socket } }, + [roomId]: [...(roomUsers[roomId] ?? []), socket.id], } io.emit('users_response', roomUsers) }) socket.on('send_message', (data: IMessage) => { - Object.values(roomUsers[data.roomId]).forEach((s) => s.emit('receive_message', data)) + Object.values(roomUsers[data.roomId]).forEach((s) => { + if (sockets[s].connected) { + sockets[s].emit('receive_message', data) + } + }) }) socket.on('disconnect', () => { @@ -49,17 +82,23 @@ io.on('connection', (socket: Socket) => { name: '', email: '', } - Object.values(roomUsers[roomId]).forEach((s) => s.emit('receive_message', msg)) + Object.values(roomUsers[roomId]).forEach((s) => { + console.log('SEdnign message to socket: ', s, sockets[s]) + if (sockets[s].connected) { + sockets[s].emit('receive_message', msg) + } + }) } } io.emit('users_response', roomUsers) }) }) + app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public', 'index.html')) }) -const PORT = 3001 +const PORT = 3010 server.listen(PORT, () => { console.log(`Server running on port ${PORT}`) diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index 6431ee6238..f778186695 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -36,17 +36,21 @@ const Chat = () => { const { id: roomId } = router.query useEffect(() => { - if (!session) { + if (!session || !roomId) { router.replace('/') return } - const socket = socketIO.connect('ws://localhost:3009') - socket.on('receive_message', (data: IMessage) => { - setChatData((prev) => { - return [...(prev ?? []), data] + if (!socket) { + const s = socketIO.connect('ws://localhost:3010') + s.emit('join_room', roomId) + s.on('receive_message', (data: IMessage) => { + console.log('Got a msg', data) + setChatData((prev) => { + return [...(prev ?? []), data] + }) }) - }) - setSocket(socket) + setSocket(s) + } }, [router, session]) const getChatBubbleFormat = (currUser: ICollaborator, type: 'label' | 'text') => { From 4a84c4a12b235e7d21f5aad5361f6be9c864c0b8 Mon Sep 17 00:00:00 2001 From: Lynn low Date: Sun, 3 Nov 2024 21:55:45 +0800 Subject: [PATCH 4/8] Update --- .../collaboration-service/src/chat-server.ts | 105 ------------------ .../src/controllers/collab.controller.ts | 12 +- .../src/models/collab.repository.ts | 4 + .../src/routes/collab.routes.ts | 3 +- .../src/services/socketio.service.ts | 25 ++++- .../collaboration-service/src/types/IChat.ts | 1 + frontend/pages/code/[id]/chat.tsx | 56 ++++------ frontend/pages/code/[id]/index.tsx | 8 +- 8 files changed, 64 insertions(+), 150 deletions(-) delete mode 100644 backend/collaboration-service/src/chat-server.ts diff --git a/backend/collaboration-service/src/chat-server.ts b/backend/collaboration-service/src/chat-server.ts deleted file mode 100644 index f5998eb040..0000000000 --- a/backend/collaboration-service/src/chat-server.ts +++ /dev/null @@ -1,105 +0,0 @@ -import express, { NextFunction, Request, Response } from 'express' -import http from 'http' -import { Server, Socket } from 'socket.io' -import cors from 'cors' -import helmet from 'helmet' -import path from 'path' - -const app = express() - -app.use(express.static(path.join(__dirname, '../public'))) -app.use(express.urlencoded({ extended: true })) -app.use(express.json()) -app.use(cors()) // config cors so that front-end can use -app.options('*', cors()) -app.use(helmet()) -app.use((request: Request, response: Response, next: NextFunction) => { - response.header('Access-Control-Allow-Origin', '*') // "*" -> Allow all links to access - - response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') - - // Browsers usually send this before PUT or POST Requests - if (request.method === 'OPTIONS') { - response.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT, PATCH') - response.status(200).send() - return - } - - // Continue Route Processing - next() -}) - -const server = http.createServer(app) -const io = new Server(server, { - cors: { - origin: '*', - methods: '*', - }, -}) - -export interface IMessage { - text: string - name: string - email: string - socketId: string - roomId: string - time: string -} - -// room id -> socket[] -let roomUsers: Record = {} -const sockets: Record = {} -io.on('connection', (socket: Socket) => { - sockets[socket.id] = socket - io.emit('users_response', roomUsers) - - socket.on('join_room', (roomId) => { - socket.join(roomId) - roomUsers = { - ...roomUsers, - [roomId]: [...(roomUsers[roomId] ?? []), socket.id], - } - io.emit('users_response', roomUsers) - }) - - socket.on('send_message', (data: IMessage) => { - Object.values(roomUsers[data.roomId]).forEach((s) => { - if (sockets[s].connected) { - sockets[s].emit('receive_message', data) - } - }) - }) - - socket.on('disconnect', () => { - for (const [roomId, users] of Object.entries(roomUsers)) { - if (Object.keys(users).includes(socket.id)) { - delete roomUsers[roomId][socket.id] - const msg: IMessage = { - text: 'A user left the room.', - socketId: 'kurakani', - roomId: roomId, - time: new Date().toString(), - name: '', - email: '', - } - Object.values(roomUsers[roomId]).forEach((s) => { - console.log('SEdnign message to socket: ', s, sockets[s]) - if (sockets[s].connected) { - sockets[s].emit('receive_message', msg) - } - }) - } - } - io.emit('users_response', roomUsers) - }) -}) - -app.get('/', (req, res) => { - res.sendFile(path.join(__dirname, '../public', 'index.html')) -}) - -const PORT = 3010 - -server.listen(PORT, () => { - console.log(`Server running on port ${PORT}`) -}) diff --git a/backend/collaboration-service/src/controllers/collab.controller.ts b/backend/collaboration-service/src/controllers/collab.controller.ts index 2a1db1d330..3060e00e21 100644 --- a/backend/collaboration-service/src/controllers/collab.controller.ts +++ b/backend/collaboration-service/src/controllers/collab.controller.ts @@ -2,7 +2,7 @@ import { ValidationError } from 'class-validator' import { Request, Response } from 'express' import { ITypedBodyRequest } from '@repo/request-types' import { CollabDto } from '../types/CollabDto' -import { createSession, getSessionById } from '../models/collab.repository' +import { createSession, getChat, getSessionById } from '../models/collab.repository' export async function createSessionRequest(request: ITypedBodyRequest, response: Response): Promise { const collabDto = CollabDto.fromRequest(request) @@ -47,3 +47,13 @@ export async function getSession(request: Request, response: Response): Promise< // Send retrieved data response.status(200).json(session).send() } + +export async function getChatHistory(request: Request, response: Response): Promise { + const id = request.params.id + + // Obtains session by _id + const chat = await getChat(id) + + // Send retrieved data + response.status(200).json(chat).send() +} diff --git a/backend/collaboration-service/src/models/collab.repository.ts b/backend/collaboration-service/src/models/collab.repository.ts index 6ba2487488..a7c10307ef 100644 --- a/backend/collaboration-service/src/models/collab.repository.ts +++ b/backend/collaboration-service/src/models/collab.repository.ts @@ -21,6 +21,10 @@ export async function getSessionById(id: string): Promise { return collabModel.findById(id) } +export async function getChat(id: string): Promise { + return collabModel.find({ _id: id }) +} + export async function updateChatHistory(id: string, chatEntry: ChatModel) { return collabModel.updateOne({ _id: id }, { $push: { chatHistory: chatEntry } }) } diff --git a/backend/collaboration-service/src/routes/collab.routes.ts b/backend/collaboration-service/src/routes/collab.routes.ts index fd055761e1..10b8b4f6b7 100644 --- a/backend/collaboration-service/src/routes/collab.routes.ts +++ b/backend/collaboration-service/src/routes/collab.routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express' import passport from 'passport' -import { createSessionRequest, getSession } from '../controllers/collab.controller' +import { createSessionRequest, getChatHistory, getSession } from '../controllers/collab.controller' const router = Router() @@ -9,5 +9,6 @@ router.use(passport.authenticate('jwt', { session: false })) // To change this route to enable retrival of sessions with pagination router.put('/', createSessionRequest) router.get('/:id', getSession) +router.get('/chat/:id', getChatHistory) export default router diff --git a/backend/collaboration-service/src/services/socketio.service.ts b/backend/collaboration-service/src/services/socketio.service.ts index 72b19b4c04..bf119cfc7f 100644 --- a/backend/collaboration-service/src/services/socketio.service.ts +++ b/backend/collaboration-service/src/services/socketio.service.ts @@ -1,8 +1,18 @@ import loggerUtil from '../common/logger.util' import { Server as IOServer, Socket } from 'socket.io' import { completeCollaborationSession } from './collab.service' -import { updateLanguage } from '../models/collab.repository' +import { updateChatHistory, updateLanguage } from '../models/collab.repository' import { LanguageMode } from '../types/LanguageMode' +import { ChatModel } from '../types' + +export interface IMessage { + text: string + name: string + email: string + socketId: string + roomId: string + time: string +} export class WebSocketConnection { private io: IOServer @@ -26,6 +36,11 @@ export class WebSocketConnection { } }) + socket.on('send_message', (data: ChatModel) => { + this.io.to(data.roomId).emit('receive_message', data) + updateChatHistory(data.roomId, data) + }) + socket.on('change-language', async (language: string) => { this.io.to(roomId).emit('update-language', language) this.languages.set(roomId, language) @@ -36,7 +51,13 @@ export class WebSocketConnection { const room = this.io.sockets.adapter.rooms.get(roomId) socket.leave(roomId) if (!this.isUserInRoom(roomId, name)) { - this.io.to(roomId).emit('user-disconnected', name) + const m: ChatModel = { + senderId: '', + message: name, + createdAt: new Date(), + roomId: roomId, + } + this.io.to(roomId).emit('user-disconnected', m) loggerUtil.info(`User ${name} disconnected from room ${roomId}`) } if (!room) { diff --git a/backend/collaboration-service/src/types/IChat.ts b/backend/collaboration-service/src/types/IChat.ts index ce88bfe03d..4ba22ffb44 100644 --- a/backend/collaboration-service/src/types/IChat.ts +++ b/backend/collaboration-service/src/types/IChat.ts @@ -8,4 +8,5 @@ export class ChatModel implements IChat { senderId: string message: string createdAt: Date + roomId: string } diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index f778186695..749bfd7350 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -1,34 +1,27 @@ -import { useEffect, useRef, useState } from 'react' +import { FC, RefObject, useEffect, useRef, useState } from 'react' -import { mockUserData } from '@/mock-data' import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import * as socketIO from 'socket.io-client' interface ICollaborator { name: string - email: string } -const userData: ICollaborator = mockUserData - const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() } export interface IMessage { - text: string - name: string - email: string - socketId: string + senderId: string + message: string + createdAt: Date roomId: string - time: string } -const Chat = () => { +const Chat: FC<{ socketRef: RefObject }> = ({ socketRef }) => { const [chatData, setChatData] = useState() - const [socket, setSocket] = useState() const chatEndRef = useRef(null) const { data: session } = useSession() const router = useRouter() @@ -36,26 +29,19 @@ const Chat = () => { const { id: roomId } = router.query useEffect(() => { - if (!session || !roomId) { - router.replace('/') - return - } - if (!socket) { - const s = socketIO.connect('ws://localhost:3010') - s.emit('join_room', roomId) - s.on('receive_message', (data: IMessage) => { + if (socketRef?.current) { + socketRef.current.on('receive_message', (data: IMessage) => { console.log('Got a msg', data) setChatData((prev) => { return [...(prev ?? []), data] }) }) - setSocket(s) } - }, [router, session]) + }, [socketRef]) const getChatBubbleFormat = (currUser: ICollaborator, type: 'label' | 'text') => { let format = '' - if (currUser.email === userData.email) { + if (currUser.name === session?.user.username) { format = 'items-end ml-5' // Add more format based on the type if (type === 'text') { @@ -80,18 +66,16 @@ const Chat = () => { } const handleSendMessage = (message: string) => { - if (!session || !socket) return + if (!session || !socketRef?.current) return if (message.trim()) { const msg: IMessage = { - text: message, - name: session.user.username, - time: new Date().toString(), - socketId: socket.id || '', + message: message, + senderId: session.user.username, + createdAt: new Date(), roomId: roomId as string, - email: session.user.email, } - socket.emit('send_message', msg) + socketRef.current.emit('send_message', msg) } setValue('') } @@ -109,16 +93,18 @@ const Chat = () => { Object.values(chatData).map((chat, index) => (
-

{chat.name}

- {formatTimestamp(chat.time)} +

{chat.senderId}

+ + {formatTimestamp(chat.createdAt.toString())} +
- {chat.text} + {chat.message}
))} diff --git a/frontend/pages/code/[id]/index.tsx b/frontend/pages/code/[id]/index.tsx index aae1ac751b..5a0e688556 100644 --- a/frontend/pages/code/[id]/index.tsx +++ b/frontend/pages/code/[id]/index.tsx @@ -1,6 +1,6 @@ import { EndIcon, PlayIcon, SubmitIcon } from '@/assets/icons' import { ITestcase, LanguageMode, getCodeMirrorLanguage } from '@/types' -import { mockChatData, mockTestCaseData, mockUserData } from '@/mock-data' +import { mockTestCaseData } from '@/mock-data' import { useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' @@ -18,7 +18,6 @@ import { Category, IMatch, SortedComplexity } from '@repo/user-types' import { useSession } from 'next-auth/react' import { getMatchDetails } from '@/services/matching-service-api' import { convertSortedComplexityToComplexity } from '@repo/question-types' -import { ReloadIcon } from '@radix-ui/react-icons' import Chat from './chat' import io, { Socket } from 'socket.io-client' import UserAvatar from '@/components/customs/custom-avatar' @@ -98,9 +97,6 @@ export default function Code() { } }, []) - // Ref for autoscroll the last chat message - const chatEndRef = useRef(null) - const toggleChat = () => { setIsChatOpen(!isChatOpen) } @@ -174,7 +170,7 @@ export default function Code() { /> - {isChatOpen && } + {isChatOpen && }
From 6d6cb52a1187f790022ed095a4e08887cbeef9b1 Mon Sep 17 00:00:00 2001 From: Lynn low Date: Sun, 3 Nov 2024 22:00:34 +0800 Subject: [PATCH 5/8] update --- frontend/pages/code/[id]/chat.tsx | 14 ++++++++++++++ frontend/services/matching-service-api.ts | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index 749bfd7350..2326fe32b8 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -3,6 +3,7 @@ import { FC, RefObject, useEffect, useRef, useState } from 'react' import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import * as socketIO from 'socket.io-client' +import { getChatHistory } from '@/services/matching-service-api' interface ICollaborator { name: string @@ -28,6 +29,19 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef const [value, setValue] = useState('') const { id: roomId } = router.query + useEffect(() => { + ;(async () => { + const matchId = router.query.id as string + if (!matchId) { + return + } + const response = await getChatHistory(matchId).catch((_) => { + router.push('/') + }) + setChatData(response) + })() + }, [router]) + useEffect(() => { if (socketRef?.current) { socketRef.current.on('receive_message', (data: IMessage) => { diff --git a/frontend/services/matching-service-api.ts b/frontend/services/matching-service-api.ts index 881e099e85..5ae1cd46de 100644 --- a/frontend/services/matching-service-api.ts +++ b/frontend/services/matching-service-api.ts @@ -32,3 +32,15 @@ export const getMatchDetails = async (matchId: string): Promise } } } + +export const getChatHistory = async (matchId: string): Promise => { + try { + return await axiosInstance.get(`/matching/chat/${matchId}`) + } catch (error) { + if (axios.isAxiosError(error)) { + throw { message: `Axios error: ${error.message}` } + } else { + throw { message: 'An unexpected error occurred' } + } + } +} From 8835cad93b7bbea1999035081b361573e8e5a4cc Mon Sep 17 00:00:00 2001 From: Lynn low Date: Sun, 3 Nov 2024 22:07:11 +0800 Subject: [PATCH 6/8] update --- frontend/pages/code/[id]/chat.tsx | 2 +- frontend/services/collaboration-service-api.ts | 8 ++++++++ frontend/services/matching-service-api.ts | 12 ------------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index 2326fe32b8..90afd2d761 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -3,7 +3,7 @@ import { FC, RefObject, useEffect, useRef, useState } from 'react' import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import * as socketIO from 'socket.io-client' -import { getChatHistory } from '@/services/matching-service-api' +import { getChatHistory } from '@/services/collaboration-service-api' interface ICollaborator { name: string diff --git a/frontend/services/collaboration-service-api.ts b/frontend/services/collaboration-service-api.ts index 9681b65147..454d5f2092 100644 --- a/frontend/services/collaboration-service-api.ts +++ b/frontend/services/collaboration-service-api.ts @@ -12,3 +12,11 @@ export const createCollabSession = async (data: ICollabSession): Promise => { + try { + return axiosInstance.get(`/collab/chat/${matchId}`) + } catch (error) { + console.error(error) + } +} diff --git a/frontend/services/matching-service-api.ts b/frontend/services/matching-service-api.ts index 5ae1cd46de..881e099e85 100644 --- a/frontend/services/matching-service-api.ts +++ b/frontend/services/matching-service-api.ts @@ -32,15 +32,3 @@ export const getMatchDetails = async (matchId: string): Promise } } } - -export const getChatHistory = async (matchId: string): Promise => { - try { - return await axiosInstance.get(`/matching/chat/${matchId}`) - } catch (error) { - if (axios.isAxiosError(error)) { - throw { message: `Axios error: ${error.message}` } - } else { - throw { message: 'An unexpected error occurred' } - } - } -} From 8cd3d6c9b5db2b46df58bff67471b02a2ca98104 Mon Sep 17 00:00:00 2001 From: Lynn low Date: Sun, 3 Nov 2024 22:14:42 +0800 Subject: [PATCH 7/8] Update --- .../src/controllers/collab.controller.ts | 11 ++++++++--- .../src/models/collab.repository.ts | 4 ---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/collaboration-service/src/controllers/collab.controller.ts b/backend/collaboration-service/src/controllers/collab.controller.ts index 3060e00e21..49dc2317a3 100644 --- a/backend/collaboration-service/src/controllers/collab.controller.ts +++ b/backend/collaboration-service/src/controllers/collab.controller.ts @@ -2,7 +2,7 @@ import { ValidationError } from 'class-validator' import { Request, Response } from 'express' import { ITypedBodyRequest } from '@repo/request-types' import { CollabDto } from '../types/CollabDto' -import { createSession, getChat, getSessionById } from '../models/collab.repository' +import { createSession, getSessionById } from '../models/collab.repository' export async function createSessionRequest(request: ITypedBodyRequest, response: Response): Promise { const collabDto = CollabDto.fromRequest(request) @@ -52,8 +52,13 @@ export async function getChatHistory(request: Request, response: Response): Prom const id = request.params.id // Obtains session by _id - const chat = await getChat(id) + const session = await getSessionById(id) + + if (!session) { + response.status(404).json(`Session with id ${id} does not exist!`).send() + return + } // Send retrieved data - response.status(200).json(chat).send() + response.status(200).json(session.chatHistory).send() } diff --git a/backend/collaboration-service/src/models/collab.repository.ts b/backend/collaboration-service/src/models/collab.repository.ts index a7c10307ef..6ba2487488 100644 --- a/backend/collaboration-service/src/models/collab.repository.ts +++ b/backend/collaboration-service/src/models/collab.repository.ts @@ -21,10 +21,6 @@ export async function getSessionById(id: string): Promise { return collabModel.findById(id) } -export async function getChat(id: string): Promise { - return collabModel.find({ _id: id }) -} - export async function updateChatHistory(id: string, chatEntry: ChatModel) { return collabModel.updateOne({ _id: id }, { $push: { chatHistory: chatEntry } }) } From 69fbce5afe028c1f9fa2bceae6d8404f3ace7159 Mon Sep 17 00:00:00 2001 From: Low Han Date: Mon, 4 Nov 2024 00:02:37 +0800 Subject: [PATCH 8/8] Clean up code --- backend/collaboration-service/package.json | 3 +-- .../src/services/socketio.service.ts | 9 ------- frontend/pages/code/[id]/chat.tsx | 27 +++++++------------ frontend/types/collaboration-api.ts | 1 + 4 files changed, 11 insertions(+), 29 deletions(-) diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json index bc8dae0fa9..5be024595b 100644 --- a/backend/collaboration-service/package.json +++ b/backend/collaboration-service/package.json @@ -9,8 +9,7 @@ "test": "jest", "lint": "eslint . --fix --no-error-on-unmatched-pattern && prettier --write --ignore-unknown .", "start": "npm run build && node dist/server.js", - "dev": "nodemon src/server.ts", - "chat": "nodemon src/chat-server.ts" + "dev": "nodemon src/server.ts" }, "license": "MIT", "dependencies": { diff --git a/backend/collaboration-service/src/services/socketio.service.ts b/backend/collaboration-service/src/services/socketio.service.ts index bf119cfc7f..f7613ca7be 100644 --- a/backend/collaboration-service/src/services/socketio.service.ts +++ b/backend/collaboration-service/src/services/socketio.service.ts @@ -5,15 +5,6 @@ import { updateChatHistory, updateLanguage } from '../models/collab.repository' import { LanguageMode } from '../types/LanguageMode' import { ChatModel } from '../types' -export interface IMessage { - text: string - name: string - email: string - socketId: string - roomId: string - time: string -} - export class WebSocketConnection { private io: IOServer private languages: Map = new Map() diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx index 90afd2d761..511b3ee40d 100644 --- a/frontend/pages/code/[id]/chat.tsx +++ b/frontend/pages/code/[id]/chat.tsx @@ -4,25 +4,15 @@ import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import * as socketIO from 'socket.io-client' import { getChatHistory } from '@/services/collaboration-service-api' - -interface ICollaborator { - name: string -} +import { IChat } from '@/types/collaboration-api' const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() } -export interface IMessage { - senderId: string - message: string - createdAt: Date - roomId: string -} - const Chat: FC<{ socketRef: RefObject }> = ({ socketRef }) => { - const [chatData, setChatData] = useState() + const [chatData, setChatData] = useState() const chatEndRef = useRef(null) const { data: session } = useSession() const router = useRouter() @@ -38,13 +28,14 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef const response = await getChatHistory(matchId).catch((_) => { router.push('/') }) + console.log('Chat history', response) setChatData(response) })() }, [router]) useEffect(() => { if (socketRef?.current) { - socketRef.current.on('receive_message', (data: IMessage) => { + socketRef.current.on('receive_message', (data: IChat) => { console.log('Got a msg', data) setChatData((prev) => { return [...(prev ?? []), data] @@ -53,9 +44,9 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef } }, [socketRef]) - const getChatBubbleFormat = (currUser: ICollaborator, type: 'label' | 'text') => { + const getChatBubbleFormat = (currUser: string, type: 'label' | 'text') => { let format = '' - if (currUser.name === session?.user.username) { + if (currUser === session?.user.username) { format = 'items-end ml-5' // Add more format based on the type if (type === 'text') { @@ -83,7 +74,7 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef if (!session || !socketRef?.current) return if (message.trim()) { - const msg: IMessage = { + const msg: IChat = { message: message, senderId: session.user.username, createdAt: new Date(), @@ -107,7 +98,7 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef Object.values(chatData).map((chat, index) => (

{chat.senderId}

@@ -116,7 +107,7 @@ const Chat: FC<{ socketRef: RefObject }> = ({ socketRef
{chat.message}
diff --git a/frontend/types/collaboration-api.ts b/frontend/types/collaboration-api.ts index 1c7103bb49..3f7eec1077 100644 --- a/frontend/types/collaboration-api.ts +++ b/frontend/types/collaboration-api.ts @@ -10,4 +10,5 @@ export interface IChat { senderId: string message: string createdAt: Date + roomId: string }