Skip to content

Commit

Permalink
Merge pull request #101 from bokuanT/branch-stopwatch
Browse files Browse the repository at this point in the history
Add synced stopwatch and tidy up video comm opening
  • Loading branch information
bokuanT authored Nov 8, 2023
2 parents 2e67647 + 9348d14 commit ac743a3
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 9 deletions.
18 changes: 18 additions & 0 deletions collaboration-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,24 @@ io.on("connection", (socket: Socket) => {
.emit("runCodeDone", results);
});

extendedSocket.on("stopwatch_start_request", () => {
io.sockets
.in(extendedSocket.roomId)
.emit("start_stopwatch");
});

extendedSocket.on("stopwatch_stop_request", () => {
io.sockets
.in(extendedSocket.roomId)
.emit("stop_stopwatch");
});

extendedSocket.on("stopwatch_reset_request", () => {
io.sockets
.in(extendedSocket.roomId)
.emit("reset_stopwatch");
});

// Handle socket disconnection
extendedSocket.on("disconnect", (reason) => {
console.log(
Expand Down
56 changes: 47 additions & 9 deletions frontend/components/CollabPage/CollabPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import {
DialogContent,
Stack,
Grid,
Fab,
} from "@mui/material";
import CodeEditor from "./CodeEditor";
import dynamic from "next/dynamic";
const VideoAudioChat = dynamic(() => import("./VideoComm"), { ssr: false });
import EndingSessionBackdrop from "./EndingSessionBackDrop";
import { messageHandler } from "@/utils/handlers";
import FabMenu from "./FabComponent/FabMenu";
import { StopwatchProps } from "./FabComponent/Stopwatch";

const CollabPage = () => {
const { language, roomId, cancelMatching, questions } =
Expand All @@ -35,14 +38,39 @@ const CollabPage = () => {
useState<boolean>(false);
const [showInterviewerView, setShowInterviewerView] = useState(false);
const [showDialog, setShowDialog] = useState(true);
const [callActive, setCallActive] = useState(true);
const user1socket = roomId.split("*-*")[0];
const user2socket = roomId.split("*-*")[1];
const [isEndingSession, setIsEndingSession] = useState(false); // If this is true, end session procedure starts (see useEffect)
const [isEndSessionHandshakeOpen, setIsEndSessionHandshakeOpen] =
useState(false);
const [iHaveAcceptedEndSession, setIHaveAcceptedEndSession] =
useState(false);
// Stopwatch stuff
const [isRunning, setIsRunning] = useState(false);
const [isReset, setIsReset] = useState(false);

const sendStartRequest = () => {
socket?.emit("stopwatch_start_request");
};

const sendStopRequest = () => {
socket?.emit("stopwatch_stop_request");
};

const sendResetRequest = () => {
socket?.emit("stopwatch_reset_request");
};

const stopwatchProps: StopwatchProps = {
isRunning: isRunning,
isReset: isReset,
setIsReset: setIsReset,
sendStartRequest: sendStartRequest,
sendStopRequest: sendStopRequest,
sendResetRequest: sendResetRequest,
setIsOpen: (x: boolean) => {}, // will be filled up by FabMenu
isOpen: false, // will be filled up by FabMenu
};

const toggleInterviewerView = () => {
setShowInterviewerView(!showInterviewerView);
Expand All @@ -57,9 +85,6 @@ const CollabPage = () => {
setShowInterviewerView(false);
}
};
const toggleVideo = () => {
setCallActive(!callActive);
};

const handleClosePickRole = (event: any, reason: string) => {
if (reason && reason == "backdropClick")
Expand Down Expand Up @@ -203,6 +228,19 @@ const CollabPage = () => {

messageHandler("End session request rejected", "warning");
});

socket.on("start_stopwatch", () => {
setIsRunning(true);
});

socket.on("stop_stopwatch", () => {
setIsRunning(false);
});

socket.on("reset_stopwatch", () => {
setIsReset(true);
setIsRunning(false);
});

return () => {
socket.disconnect();
Expand Down Expand Up @@ -308,11 +346,11 @@ const CollabPage = () => {
</Dialog>
</Grid>
</Grid>
<VideoAudioChat username1={user1socket}
username2={user2socket}
callActive={callActive}
setCallActive={setCallActive}
/>
<FabMenu
stopwatchProps={stopwatchProps}
username1={user1socket}
username2={user2socket}
/>
{isEndingSession && <EndingSessionBackdrop />}

</div>
Expand Down
87 changes: 87 additions & 0 deletions frontend/components/CollabPage/FabComponent/FabMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState } from "react";
import { Fab, Menu, MenuItem } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { StopwatchProps } from "./Stopwatch";
import Stopwatch from "./Stopwatch";
import dynamic from "next/dynamic";
const VideoAudioChat = dynamic(() => import("../VideoComm"), { ssr: false });

interface FabMenuProps {
stopwatchProps: StopwatchProps;
username1: string | null;
username2: string;
}

const FabMenu = ({ stopwatchProps, username1, username2 }: FabMenuProps) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [stopwatchOpen, setStopwatchOpen] = React.useState(false);
const open = Boolean(anchorEl);
const [callActive, setCallActive] = useState(true);

const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

const handleClickVideo = () => {
setAnchorEl(null);
setCallActive(true);
};

const handleClickStopwatch = () => {
setStopwatchOpen(true);
setAnchorEl(null);
};

const handleClose = () => {
setAnchorEl(null);
};

const handleCloseStopwatch = () => {
setStopwatchOpen(false);
};

stopwatchProps.setIsOpen = setStopwatchOpen;
stopwatchProps.isOpen = stopwatchOpen;

return (
<div>
<Fab
color="primary"
aria-label="add"
style={{
position: "fixed",
bottom: "20px",
right: "20px",
}}
onClick={handleClick}
>
<MenuIcon />
</Fab>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<MenuItem onClick={handleClickVideo}>Video</MenuItem>
<MenuItem onClick={handleClickStopwatch}>Stopwatch</MenuItem>
</Menu>
<VideoAudioChat
username1={username1}
username2={username2}
callActive={callActive}
setCallActive={setCallActive}
/>
<Stopwatch {...stopwatchProps} />
</div>
);
};

export default FabMenu;
180 changes: 180 additions & 0 deletions frontend/components/CollabPage/FabComponent/Stopwatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Source: https://github.com/jinderbrar/Stopwatch-using-ReactJS-and-Material-UI/blob/main/src/components/Stopwatch.js
import React, { useState, useRef, useEffect } from "react";
import {
Box,
Typography,
IconButton,
Grid,
Tooltip,
} from "@mui/material";
import Draggable from "react-draggable";
import PlayCircleFilledIcon from "@mui/icons-material/PlayCircleFilled";
import PauseCircleFilledIcon from "@mui/icons-material/PauseCircleFilled";
import MinimizeIcon from '@mui/icons-material/Minimize';
import RestoreIcon from "@mui/icons-material/Restore";

export interface StopwatchProps {
isRunning: boolean;
isReset: boolean;
setIsReset: (isReset: boolean) => void;
sendStartRequest: () => void;
sendStopRequest: () => void;
sendResetRequest: () => void;
setIsOpen: (isOpen: boolean) => void;
isOpen: boolean;
}

const Stopwatch = ({
isRunning,
isReset,
setIsReset,
sendStartRequest,
sendStopRequest,
sendResetRequest,
setIsOpen,
isOpen }: StopwatchProps) => {
const [time, setTime] = useState<number>(0.0);
const [isActive, setIsActive] = useState<boolean>(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);

const formatTime = () => {
const sec = `${Math.floor(time) % 60}`.padStart(2, "0");
const min = `${Math.floor(time / 60) % 60}`.padStart(2, "0");
const hour = `${Math.floor(time / 3600)}`.padStart(2, "0");
return (
<>
<Typography variant="h1">{[hour, min, sec].join(":")}</Typography>
<Box sx={{ display: "flex", flexDirection: "row", justifyContent: "space-around" }}>
{["hr", "min", "sec"].map((unit) => (
<Typography key={unit} variant="overline">
{unit}
</Typography>
))}
</Box>
</>
);
};

const handlePlayPause = () => {
if (isActive) {
sendStopRequest();
} else {
sendStartRequest();
}
};
const handleReset = () => {
sendResetRequest();
};

const onClose = () => {
setIsOpen(false);
};

useEffect(() => {
if (isRunning) {
setIsActive(true);
} else {
setIsActive(false);
}

}, [isRunning]);

useEffect(() => {
if (isReset) {
setIsReset(false);
setTime(0);
}
}, [isReset]);

useEffect(() => {
if (isActive) {
intervalRef.current = setInterval(() => setTime((prevTime) => prevTime + 0.1), 100);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}
}, [isActive]);

return (
<div className={isOpen ? "stopwatch" : "hidden"}>
<Draggable>
<Box
sx={{
position: 'absolute', // Centering in the viewport
top: '50%', // Align vertically
left: '50%', // Align horizontally
transform: 'translate(-50%, -50%)', // Adjust the position to center
bgcolor: 'background.paper', // Use theme's paper color for background
boxShadow: 3, // Apply some shadow
p: 4, // Padding around the content
borderRadius: 'borderRadius' // Optional: rounded corners
}}
>
{/* Close button at the top right corner */}
<Box
sx={{
position: 'absolute',
top: 16,
right: 16,
}}
>
<IconButton onClick={onClose}>
<MinimizeIcon />
</IconButton>
</Box>
<Grid m={2} sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<Grid item>{formatTime()}</Grid>
<Grid item>
<ControlButtons
args={{
time,
isActive,
handlePlayPause,
handleReset
}}
/>
</Grid>
</Grid>
</Box>
</Draggable>
</div>
);
};

type ControlButtonsProps = {
args: {
time: number;
isActive: boolean;
handlePlayPause: () => void;
handleReset: () => void;
};
};

const ControlButtons: React.FC<ControlButtonsProps> = ({
args: { time, isActive, handlePlayPause, handleReset }
}) => {
return (
<>
{/* play or pause stopwatch */}
<Tooltip title={isActive ? "Pause" : "Play"}>
<IconButton onClick={() => handlePlayPause()}>
{isActive ? (
<PauseCircleFilledIcon sx={{ color: "palette.status.pause", fontSize: "48px" }} />
) : (
<PlayCircleFilledIcon sx={{ color: "palette.status.play", fontSize: "48px" }} />
)}
</IconButton>
</Tooltip>
{/* reset stopwatch */}
<Tooltip title="Reset">
<IconButton disabled={time === 0} onClick={() => handleReset()}>
<RestoreIcon sx={{ fontSize: "48px" }} />
</IconButton>
</Tooltip>
</>
);
};

export default Stopwatch;

0 comments on commit ac743a3

Please sign in to comment.