Skip to content

Commit ea9fc30

Browse files
authored
Merge pull request #46 from CS3219-AY2425S1/collab-space-notif
Collab Space Patch: Notifications, CodeSpace
2 parents d2c22fb + b84c1d8 commit ea9fc30

File tree

5 files changed

+114
-28
lines changed

5 files changed

+114
-28
lines changed

Backend/CollabService/utils/roomManager.js

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const rooms = {}; // { roomId: [sockets] }
44
function manageRoom(ws, roomId, userId, type) {
55
console.log(`manageRoom function: ${type} ${userId}`)
66

7+
ws.hasLeft = false;
8+
79
switch (type) {
810
case "join":
911

@@ -47,22 +49,12 @@ function manageRoom(ws, roomId, userId, type) {
4749

4850
// notify users of roomId of updated user list to display in frontend
4951
broadcastUserListUpdate(roomId);
52+
// notify users of user joining the room
53+
broadcastUserJoin(roomId, userId);
5054
break;
5155

5256
case "leave":
53-
// remove from room
54-
rooms[roomId].sockets = rooms[roomId].sockets.filter(socket => socket !== ws);
55-
rooms[roomId].userIds = rooms[roomId].userIds.filter(user => user !== userId);
56-
57-
console.log(`User ${userId} left the room ${roomId}`);
58-
59-
// if no one in room delete room
60-
if (rooms[roomId].sockets.length === 0) {
61-
delete rooms[roomId];
62-
console.log(`Room ${roomId} is empty and deleted`);
63-
} else {
64-
broadcastUserListUpdate(roomId);
65-
}
57+
handleUserLeave(ws, roomId, userId);
6658
break;
6759
default:
6860
console.error(`Unknown room management type: ${type}`);
@@ -71,24 +63,29 @@ function manageRoom(ws, roomId, userId, type) {
7163

7264
// Remove user when they disconnect
7365
ws.on('close', () => {
66+
handleUserLeave(ws, roomId, userId)
67+
68+
});
69+
70+
function handleUserLeave(ws, roomId, userId, leaveType) {
71+
if (ws.hasLeft) return;
72+
ws.hasLeft = true;
7473

7574
if (rooms[roomId]) {
7675
rooms[roomId].sockets = rooms[roomId].sockets.filter(client => client !== ws);
77-
rooms[roomId].userIds = rooms[roomId].userIds.filter(user => user != userId);
76+
rooms[roomId].userIds = rooms[roomId].userIds.filter(user => user !== userId);
7877

7978
console.log(`User ${userId} left room ${roomId}`);
8079

8180
if (rooms[roomId].sockets.length === 0) {
8281
delete rooms[roomId];
8382
console.log(`Room ${roomId} is empty and deleted`);
8483
} else {
85-
// notify the remaining party about leave
8684
broadcastUserListUpdate(roomId);
85+
broadcastUserLeft(roomId, userId);
8786
}
88-
8987
}
90-
91-
});
88+
}
9289

9390
function broadcastUserListUpdate(roomId) {
9491
const userList = rooms[roomId].userIds;
@@ -99,6 +96,24 @@ function manageRoom(ws, roomId, userId, type) {
9996
}
10097
})
10198
}
99+
100+
function broadcastUserLeft(roomId, userId) {
101+
102+
rooms[roomId].sockets.forEach((client) => {
103+
if (client.readyState == WebSocket.OPEN) {
104+
client.send(JSON.stringify({ type: 'userLeft', user: userId}));
105+
}
106+
})
107+
}
108+
109+
function broadcastUserJoin(roomId, userId) {
110+
111+
rooms[roomId].sockets.forEach((client) => {
112+
if (client.readyState == WebSocket.OPEN) {
113+
client.send(JSON.stringify({ type: 'userJoin', user: userId}));
114+
}
115+
})
116+
}
102117
}
103118

104119
function getRoom(roomId) {

Backend/CollabService/websocket/roomManagerSocket.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ function setupWebSocket(server) {
3131
console.log('got message ', data.message);
3232
broadcastMessage(data.roomId, data.message);
3333
break;
34+
case 'languageChange':
35+
// send message to all users in room
36+
console.log('got language change ', data.language);
37+
broadcastLanguageChange(data.roomId, data.language, data.user);
38+
break;
3439
default:
3540
console.error('Unknown message type');
3641
}
@@ -52,6 +57,17 @@ function setupWebSocket(server) {
5257
}
5358
}
5459

60+
function broadcastLanguageChange(roomId, language, userId) {
61+
const room = getRoom(roomId)
62+
if (room) {
63+
room.sockets.forEach((client) => {
64+
if (client.readyState === WebSocket.OPEN) {
65+
client.send(JSON.stringify({ type: 'languageChange', language: language , user: userId}));
66+
}
67+
})
68+
}
69+
}
70+
5571
}
5672

5773
module.exports = { setupWebSocket };

Frontend/src/components/collab/CodeSpace.jsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react'
22
import { Editor } from '@monaco-editor/react'
3-
import { Container, Stack } from 'react-bootstrap'
3+
import { Container, Stack, Spinner } from 'react-bootstrap'
44

5-
const CodeSpace = ({ handleEditorChange, code, language, output }) => {
5+
const CodeSpace = ({ handleEditorChange, loading, code, language, output }) => {
66
return (
77

88
<Stack gap={3} className='h-100'>
@@ -16,7 +16,14 @@ const CodeSpace = ({ handleEditorChange, code, language, output }) => {
1616
/>
1717
<Container style={{ height: '200px', border: '1px solid #ccc', borderRadius: '0.5rem', padding: '1rem', backgroundColor: '#f8f9fa'}}>
1818
<h5>Output</h5>
19-
<p style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap'}}>{output}</p>
19+
{loading ? (
20+
<div className="d-flex justify-content-center align-items-center" style={{ height: '100%' }}>
21+
<Spinner animation="border" variant="primary" />
22+
<span className="ms-1">Running...</span>
23+
</div>
24+
): (
25+
<p style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap'}}>{output}</p>
26+
)}
2027
</Container>
2128
</Stack>
2229

Frontend/src/components/collab/CollabNavigationBar.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import React from 'react';
22
import { Button, Container, Navbar, Nav, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'react-bootstrap';
33

4-
const CollabNavigationBar = ({ handleExit, users, handleCodeRun, setLanguage, language }) => {
4+
const CollabNavigationBar = ({ handleExit, users, handleCodeRun, setLanguage, language, userLangChange }) => {
55

66
return (
7-
<Navbar className='bg-light' sticky='top'>
7+
<Navbar className='bg-light' sticky='top' style={{ zIndex: 1040 }}>
88
<Container className='d-flex justify-content-between'>
99

1010
{/* Language Dropdown on left */}
1111
<Nav className='me-auto'>
12-
<Dropdown onSelect={(eventKey) => setLanguage(eventKey)}>
12+
<Dropdown onSelect={(eventKey) => {
13+
setLanguage(eventKey);
14+
userLangChange(eventKey);
15+
}}>
1316
<DropdownToggle style={{ backgroundColor: 'transparent', border: '1px solid #ccc', borderRadius: '0', color: '#000', padding: '8px 16px'}}>
1417
{language}
1518
</DropdownToggle>

Frontend/src/components/collab/CollaborationSpace.jsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const CollaborationSpace = () => {
2525
const [language, setLanguage] = useState("python") // set default language to python
2626
const [output, setOutput] = useState("")
2727
const [messages, setMessages] = useState([])
28+
const [outputLoading, setOutputLoading] = useState(false)
2829

2930
// use https://emkc.org/api/v2/piston/runtimes to GET other languages
3031
const LANGUAGEVERSIONS = {
@@ -33,6 +34,7 @@ const CollaborationSpace = () => {
3334
"c++": "10.2.0"
3435
};
3536

37+
{/* State management for access denied toast */}
3638
const [showAccessDeniedToast, setShowAccessDeniedToast] = useState(false);
3739
const [toastMessage, setToastMessage] = useState('');
3840
const [loading, setLoading] = useState(true);
@@ -42,6 +44,18 @@ const CollaborationSpace = () => {
4244
navigate("/home");
4345
};
4446

47+
{/* State management for user join/leave toast */}
48+
const [notifs, setNotifs] = useState([]);
49+
50+
const addNotif = (message) => {
51+
const id = Date.now(); // unique id based on timestamp
52+
setNotifs((prevNotifs) => [...prevNotifs, {id, message}]);
53+
54+
// Remove notif after 2 seconds
55+
setTimeout(() => {
56+
setNotifs((prevNotifs) => prevNotifs.filter((notif) => notif.id !== id))
57+
}, 1500);
58+
};
4559

4660
{/* Set up websockets for room management on client side, and collaboration for Yjs */}
4761
useEffect(() => {
@@ -87,7 +101,7 @@ const CollaborationSpace = () => {
87101
// on getting a reply from server
88102
websocket.onmessage = (event) => {
89103
const data = JSON.parse(event.data);
90-
console.log(`[FRONTEND] data message is ${JSON.stringify(data)}`);
104+
// console.log(`[FRONTEND] data message is ${JSON.stringify(data)}`);
91105
switch (data.type) {
92106
case 'usersListUpdate':
93107
setUsers(data.users); // Update the user list
@@ -101,7 +115,16 @@ const CollaborationSpace = () => {
101115
break;
102116
case 'newMessage':
103117
console.log("adding message", data.message)
104-
setMessages((prevMessages) => [...prevMessages, data.message]);
118+
break;
119+
case 'languageChange':
120+
addNotif(`User ${data.user} has changed the language to ${data.language}`);
121+
setLanguage(data.language);
122+
break;
123+
case 'userJoin':
124+
addNotif(`User ${data.user} has joined.`)
125+
break;
126+
case 'userLeft':
127+
addNotif(`User ${data.user} has left`)
105128
break;
106129
default:
107130
console.log("No messages received from room management server");
@@ -147,6 +170,9 @@ const CollaborationSpace = () => {
147170
};
148171

149172
const handleCodeRun = () => {
173+
174+
setOutputLoading(true);
175+
150176
const code_message = {
151177
"language": language,
152178
"files": [
@@ -159,6 +185,7 @@ const CollaborationSpace = () => {
159185

160186
collabService.getCodeOutput(code_message)
161187
.then(result => {
188+
setOutputLoading(false);
162189
console.log(result.data.run.output)
163190
setOutput(result.data.run.output)
164191
})
@@ -176,6 +203,11 @@ const CollaborationSpace = () => {
176203
websocketRef.current.send(JSON.stringify({ type: 'sendMessage', roomId: roomId, message: message}));
177204
}
178205

206+
const handleLanguageChange = (value) => {
207+
websocketRef.current.send(JSON.stringify({ type: 'languageChange', roomId: roomId,
208+
user: userId, language: value }));
209+
}
210+
179211
if (loading) {
180212
return (
181213
<div style={{ textAlign: 'center' }}>
@@ -205,11 +237,24 @@ const CollaborationSpace = () => {
205237
</ToastContainer>
206238
) : (
207239
<>
208-
<CollabNavigationBar handleExit={handleExit} handleCodeRun={handleCodeRun} users={users} setLanguage={setLanguage} language={language}/>
240+
{/* Toast Container for Join/Leave notifications */}
241+
<ToastContainer className='p-3' position='top-center' style={{ zIndex: 1050 }}>
242+
{notifs.map((notif) => (
243+
<Toast key={notif.id} className='mb-2' style={{ backgroundColor: 'rgba(255, 255, 255, 0.8)' }}>
244+
<Toast.Body>
245+
<strong className='text-black'>{notif.message}</strong>
246+
</Toast.Body>
247+
</Toast>
248+
))}
249+
</ToastContainer>
250+
251+
{/* Main component content */}
252+
<CollabNavigationBar handleExit={handleExit} handleCodeRun={handleCodeRun} users={users}
253+
setLanguage={setLanguage} language={language} userLangChange={handleLanguageChange}/>
209254
<Container fluid style={{ marginTop: '20px' }}>
210255
<Row>
211256
<Col md={8}>
212-
<CodeSpace handleEditorChange={handleEditorChange} code={code} language={language} output={output}/>
257+
<CodeSpace handleEditorChange={handleEditorChange} loading={outputLoading} code={code} language={language} output={output}/>
213258
</Col>
214259
<Col md={4}>
215260
<QuestionDisplay/>

0 commit comments

Comments
 (0)