diff --git a/debug.log b/debug.log index 79c61558..580915a9 100644 --- a/debug.log +++ b/debug.log @@ -1,3 +1,5 @@ +[1022/141204.980:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) + [1009/090500.006:ERROR:process_reader_win.cc(151)] SuspendThread: Access is denied. (0x5) [1009/090500.011:ERROR:process_reader_win.cc(151)] SuspendThread: Access is denied. (0x5) [1009/090500.012:ERROR:process_reader_win.cc(151)] SuspendThread: Access is denied. (0x5) diff --git a/src/client/canvas/DrawingCanvas.tsx b/src/client/canvas/DrawingCanvas.tsx index cf4a3915..a46efeb9 100644 --- a/src/client/canvas/DrawingCanvas.tsx +++ b/src/client/canvas/DrawingCanvas.tsx @@ -317,7 +317,7 @@ export const DrawingCanvas: React.FunctionComponent = (props: Props) => { { diff --git a/src/client/chat/Chat.less b/src/client/chat/Chat.less index 90b73125..0e86d58d 100644 --- a/src/client/chat/Chat.less +++ b/src/client/chat/Chat.less @@ -1,6 +1,6 @@ .chat-container { .chat-log-container { - height: 300px; + height: 100%; overflow-y: auto; overflow-x: hidden; .message-container { diff --git a/src/client/chat/ChatContainer.tsx b/src/client/chat/ChatContainer.tsx index 8d3ab00e..ddfa0e3f 100644 --- a/src/client/chat/ChatContainer.tsx +++ b/src/client/chat/ChatContainer.tsx @@ -99,9 +99,9 @@ export const ChatContainer: React.FunctionComponent = (props: Props) => { - +
- + = (props: Props) => { /> - diff --git a/src/client/chat/ChatLog.tsx b/src/client/chat/ChatLog.tsx index 2bb0e43e..30a1ba7b 100644 --- a/src/client/chat/ChatLog.tsx +++ b/src/client/chat/ChatLog.tsx @@ -18,7 +18,11 @@ export const ChatLog: React.FunctionComponent = (props: Props) => { const scrollAnchorRef = React.useRef(null); React.useEffect(() => { - scrollAnchorRef.current?.scrollIntoView({ behavior: "smooth" }); + scrollAnchorRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "start", + }); }, [messages]); return ( diff --git a/src/client/chat/ChatSession.tsx b/src/client/chat/ChatSession.tsx index 1ddd252d..d02ff0a7 100644 --- a/src/client/chat/ChatSession.tsx +++ b/src/client/chat/ChatSession.tsx @@ -187,7 +187,11 @@ export const ChatSession: React.FunctionComponent = (props: Props) => { /> - diff --git a/src/client/filehandler/FileModal.tsx b/src/client/filehandler/FileModal.tsx index cff3db7c..945bf741 100644 --- a/src/client/filehandler/FileModal.tsx +++ b/src/client/filehandler/FileModal.tsx @@ -21,30 +21,20 @@ type Props = { }>; roomType: RoomType; isLoading?: boolean; - size?: "sm" | "lg"; + setShowModal: Function; + showModal: boolean; }; /** * Similar to the file container but is rendered in an file modal. */ export const FileModal = (props: Props) => { - const [showModal, setShowModal] = React.useState(false); - return (
- { - setShowModal(false); + props.setShowModal(false); }} size="xl" scrollable={true} @@ -67,7 +57,7 @@ export const FileModal = (props: Props) => { + +

+ {`${sessionResponse.data.courseCode} - ${sessionResponse.data.roomType}`} +

+ + + +
+ + +

{ + setAltView(false); + }} + > + Speaker View +

+

{ + setAltView(true); + }} + > + Participants View +

+
+
+ + + + + + + {altView ? null : presenterView} +
+ {altView ? presenterView : null} + {altView ? video : null}
- - - - - - - {/* */} - - - - - - - - - - - {props.userData.userType === - UserType.COORDINATOR && ( - - )} - - + + {altView ? participants : video}
-
- + + + + + + + + + + + + + + + {isStaff(userType) && ( + + )} + + + + {props.userData.userType === + UserType.COORDINATOR && ( + + )} + + + + - + - { - setBreakoutRoomModalVisible(false); - }} - /> - { - setBreakoutRoomListModalVisible(false); - }} - /> - { - setBreakoutAllocationEventData(undefined); - }} - /> - { - setResponsesModalStatus((prev) => { - return { - ...prev, - visible: false, - }; - }); - }} - modalType={responsesModalStatus.type} - /> - + + { + setBreakoutRoomModalVisible(false); + }} + /> + { + setBreakoutRoomListModalVisible(false); + }} + /> + { + setBreakoutAllocationEventData(undefined); + }} + /> + { + setResponsesModalStatus((prev) => { + return { + ...prev, + visible: false, + }; + }); + }} + modalType={responsesModalStatus.type} + > + ); }; diff --git a/src/client/rooms/PrivateRoom.less b/src/client/rooms/PrivateRoom.less index 92a7d911..4a2fc385 100644 --- a/src/client/rooms/PrivateRoom.less +++ b/src/client/rooms/PrivateRoom.less @@ -60,3 +60,8 @@ } } } + +.room-content-container { + min-height: 600px; + max-height: 800px; +} diff --git a/src/client/rooms/PrivateRoomContainer.tsx b/src/client/rooms/PrivateRoomContainer.tsx index 6f803ea2..efd2769f 100644 --- a/src/client/rooms/PrivateRoomContainer.tsx +++ b/src/client/rooms/PrivateRoomContainer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button, Col, Container, Row } from "react-bootstrap"; +import { Button, Col, Container, Row, ButtonGroup } from "react-bootstrap"; import * as AiIcons from "react-icons/ai"; import { Link, RouteComponentProps } from "react-router-dom"; import { @@ -16,6 +16,8 @@ import { requestIsLoaded } from "../utils"; import { ScreenSharingContainer } from "../videostreaming/ScreenSharingContainer"; import { SessionContainer, SidePanelContainer } from "./containers"; import "./PrivateRoom.less"; +import { StreamControl } from "./components"; +import { useScreenSharing } from "../hooks/useScreenSharing"; type Props = RouteComponentProps<{ roomId: string }> & TopLayerContainerProps & { @@ -52,6 +54,13 @@ export const PrivateRoomContainer: React.FunctionComponent = ( true ); + const { setupScreenSharing, stopScreenSharing } = useScreenSharing( + props.userData.id, + roomId + ); + + const [showFileModal, setShowFileModal] = React.useState(false); + const [files, setFiles] = React.useState< Array<{ id: string; @@ -82,23 +91,7 @@ export const PrivateRoomContainer: React.FunctionComponent = ( ) => { return ( <> - {props.roomType === RoomType.BREAKOUT && ( - - - - )} - +
@@ -117,6 +110,17 @@ export const PrivateRoomContainer: React.FunctionComponent = ( Back + + +

{`${sessionData.name}`}

@@ -156,34 +160,45 @@ export const PrivateRoomContainer: React.FunctionComponent = (
- + {showCanvas ? ( ) : ( - + )} - + + + - + = (
+ ); }} diff --git a/src/client/rooms/classroom.less b/src/client/rooms/classroom.less index f53b61c8..33309075 100644 --- a/src/client/rooms/classroom.less +++ b/src/client/rooms/classroom.less @@ -7,8 +7,18 @@ .hand { position: absolute; top: 0px; - right: -10px; - color: mediumpurple; + left: 70px; + color: @content-bg; + } + &.lg { + img, + .stream-video-wrapper { + width: 100px; + height: 100px; + } + .username { + color: black; + } } } @@ -18,13 +28,18 @@ flex-wrap: wrap; position: relative; img, - video { + .stream-video-wrapper { width: 50px; height: 50px; + background-repeat: no-repeat; background-position: center; background-size: cover; border-radius: 50%; + overflow: hidden; + .stream-video { + object-fit: cover; + } &.student { border: 1px solid mediumpurple; } @@ -47,11 +62,68 @@ .classroom-container { display: flex; min-height: 100vh; - .stream-container { - height: 600px; +} +.video-container { + height: 600px; +} + +.presenter-container { + .presenter-picture { + width: 300px; + height: 300px; + margin: auto; + border-radius: 50px; + overflow: hidden; + img { + height: 100%; + width: 100%; + } } +} + +.video-alt { .video-container { + height: 250px; + width: 400px; + } + .presenter-container { + font-family: Raleway; + padding-top: 20% !important; + } + .presenter-picture { + min-width: 100px; + min-height: 100px; + width: 5vw; + height: 5vw; + margin: auto; + border-radius: 200px; background-color: lightblue; - height: 500px; + } + .presenter-name { + font-family: Raleway; + font-weight: 600; + font-size: 25px; + margin: auto; + text-align: center; + } + .presenter-role { + font-family: Raleway; + font-weight: 200; + font-size: 15px; + margin: auto; + text-align: center; + } +} + +.alt-view-users { + height: 500px; + width: 900px; + margin-left: 0; + .user-display { + padding: 25px; + img { + height: 100px; + width: 100px; + } } } diff --git a/src/client/rooms/components/BreakoutRoomListModal.tsx b/src/client/rooms/components/BreakoutRoomListModal.tsx index 54282e5f..3a3babc5 100644 --- a/src/client/rooms/components/BreakoutRoomListModal.tsx +++ b/src/client/rooms/components/BreakoutRoomListModal.tsx @@ -49,6 +49,11 @@ export const BreakoutRoomListModal: React.FunctionComponent = ( {requestIsLoaded(breakoutRoomResponse) ? ( + {breakoutRoomResponse.data.rooms.length === 0 && ( +

+ There are no breakout rooms available +

+ )} {breakoutRoomResponse.data.rooms.map((room, index) => { return ( diff --git a/src/client/rooms/components/HostDisplay.tsx b/src/client/rooms/components/HostDisplay.tsx new file mode 100644 index 00000000..46451ba3 --- /dev/null +++ b/src/client/rooms/components/HostDisplay.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { UserData } from "../../../types"; +import { Loader, ProfilePicture } from "../../components"; +import { useFetch } from "../../hooks"; +import { requestIsLoaded, userTypeToClass } from "../../utils"; +import { PeerContext } from "../../peer"; +import { MyVideo } from "../../videostreaming"; + +type Props = { + hostId?: string; + myUserId: string; +}; + +export const HostDisplay: React.FunctionComponent = (props: Props) => { + const { peer: myPeer, stream: myStream } = React.useContext(PeerContext); + const [userResponse] = useFetch( + "/user/getUserById", + { userID: props.hostId } + ); + + if (!requestIsLoaded(userResponse)) { + return ; + } + + return ( +
+
+ {myPeer && myStream ? ( + + ) : ( + props.hostId && ( + + ) + )} +
+

{userResponse.data.username}

+

+ Course {userTypeToClass(userResponse.data.userType)} +

+
+ ); +}; diff --git a/src/client/rooms/components/Participants.tsx b/src/client/rooms/components/Participants.tsx index 1c7b4e3c..ee405ad0 100644 --- a/src/client/rooms/components/Participants.tsx +++ b/src/client/rooms/components/Participants.tsx @@ -1,6 +1,6 @@ import React, { useContext } from "react"; import { Row } from "react-bootstrap"; -import { GiHand } from "react-icons/gi"; +import { FaHandPaper } from "react-icons/fa"; import { UserDataResponseType } from "../../../types"; import { PeerContext } from "../../peer"; import { UserDisplay } from "../components"; @@ -14,6 +14,8 @@ type Props = { raisedHandUsers: Array; // Current user id. myUserId: string; + // Large or small user profiles? + size?: "lg" | "sm"; }; /** @@ -28,7 +30,7 @@ export const Participants: React.FunctionComponent = (props: Props) => { const handIcon = React.useCallback( (user: { id: string }) => { return raisedHandUsers.includes(user.id) ? ( - + ) : ( <> ); @@ -37,8 +39,10 @@ export const Participants: React.FunctionComponent = (props: Props) => { ); return ( - -
+ +
{props.users.map((user) => { if (user.id === props.myUserId) { if (myPeer && myStream) { diff --git a/src/client/rooms/components/Progress.tsx b/src/client/rooms/components/Progress.tsx index a6e2fae7..96a8a135 100644 --- a/src/client/rooms/components/Progress.tsx +++ b/src/client/rooms/components/Progress.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Container } from "react-bootstrap"; +import { Container, Row } from "react-bootstrap"; import format from "date-fns/format"; type Props = { @@ -14,40 +14,66 @@ type Props = { */ export const Progress: React.FunctionComponent = (props: Props) => { const { startTime, endTime } = props; - + const startTimeSeconds = startTime.getTime() / 1000; + const endTimeSeconds = endTime.getTime() / 1000; const [progress, setProgress] = React.useState( - props.startTime.getTime() + new Date().getTime() / 1000 ); React.useEffect(() => { - const timeoutRef = setTimeout(() => { + const intervalRef = setInterval(() => { setProgress((prev) => { return prev + 1; }); }, 1000); return () => { - clearTimeout(timeoutRef); + clearTimeout(intervalRef); }; }, []); const progressPercent = React.useMemo(() => { - return progress / endTime.getTime(); - }, [progress, endTime]); + return ( + 100 - + ((endTimeSeconds - progress) / + (endTimeSeconds - startTimeSeconds)) * + 100 + ); + }, [progress, endTimeSeconds, startTimeSeconds]); + + const progressText = React.useMemo(() => { + if (progressPercent < 0) { + return "Class will start soon..."; + } else if (progressPercent > 0 && progressPercent < 100) { + return "Class in progress..."; + } else { + return "Class over..."; + } + }, [progressPercent]); return ( - - - {format(startTime, "hh:mm")} - -
-
-
- {format(endTime, "hh:mm")} + + +

{progressText}

+
+ + + {format(startTime, "dd/mm hh:mm")} + +
+
+
+ + {format(endTime, "dd/mm hh:mm")} + +
); }; diff --git a/src/client/rooms/components/StreamControl.tsx b/src/client/rooms/components/StreamControl.tsx new file mode 100644 index 00000000..3211613b --- /dev/null +++ b/src/client/rooms/components/StreamControl.tsx @@ -0,0 +1,145 @@ +import React from "react"; +import { ButtonGroup, Button } from "react-bootstrap"; +import * as BiIcons from "react-icons/bi"; +import { + MdPhonelink, + MdPhonelinkOff, + MdFiberSmartRecord, +} from "react-icons/md"; +import { PeerContext } from "../../peer"; +import { + turnVideoOff, + turnAudioOn, + turnVideoOn, + turnAudioOff, +} from "../../hooks/useMediaStream"; +import { useScreenRecording } from "../../hooks/useScreenRecording"; +import { AiOutlineVideoCameraAdd } from "react-icons/ai"; + +type Props = { + setupScreenSharing: () => Promise; + stopScreenSharing: () => void; + disableStaffControl?: boolean; +}; + +/** + * Component for controlling stream elements such as camera, mic etc. + */ +export const StreamControl: React.FunctionComponent = (props: Props) => { + const { setupScreenSharing, stopScreenSharing } = props; + const { stream } = React.useContext(PeerContext); + const [beginRecording, stopAndSaveRecording] = useScreenRecording(); + + const [cameraEnabled, setCameraEnabled] = React.useState(false); + const [cameraAudioEnabled, setCameraAudioEnabled] = React.useState( + false + ); + const [screenEnabled, setScreenEnabled] = React.useState(false); + const [isRecording, setIsRecording] = React.useState(false); + + React.useEffect(() => { + if (stream) { + if (cameraEnabled) { + turnVideoOn(stream); + } else { + turnVideoOff(stream); + } + } + }, [cameraEnabled, stream]); + + React.useEffect(() => { + if (stream) { + if (cameraAudioEnabled) { + turnAudioOn(stream); + } else { + turnAudioOff(stream); + } + } + }, [cameraAudioEnabled, stream]); + + return ( + + + + + + + ); +}; diff --git a/src/client/rooms/components/index.tsx b/src/client/rooms/components/index.tsx index a18a35b8..7f22dc9b 100644 --- a/src/client/rooms/components/index.tsx +++ b/src/client/rooms/components/index.tsx @@ -6,3 +6,5 @@ export * from "./Participants"; export * from "./ScheduleRoomForm"; export * from "./CreatePrivateRoomForm"; export * from "./Progress"; +export * from "./StreamControl"; +export * from "./HostDisplay"; diff --git a/src/client/rooms/containers/ParticipantsContainer.tsx b/src/client/rooms/containers/ParticipantsContainer.tsx index a63bfbad..d2bbc342 100644 --- a/src/client/rooms/containers/ParticipantsContainer.tsx +++ b/src/client/rooms/containers/ParticipantsContainer.tsx @@ -39,9 +39,10 @@ export const ParticipantsContainer: React.FunctionComponent = ( return ( - + { setFilterValue(e.target.value); diff --git a/src/client/rooms/containers/SessionContainer.tsx b/src/client/rooms/containers/SessionContainer.tsx index 9ac02073..1532645b 100644 --- a/src/client/rooms/containers/SessionContainer.tsx +++ b/src/client/rooms/containers/SessionContainer.tsx @@ -91,7 +91,7 @@ export const SessionContainer: React.FunctionComponent = ( return ( - + {children( sessionResponse.data, sessionUsersResponse.data?.users, diff --git a/src/client/rooms/containers/SidePanelContainer.less b/src/client/rooms/containers/SidePanelContainer.less index 092cc921..a13fa802 100644 --- a/src/client/rooms/containers/SidePanelContainer.less +++ b/src/client/rooms/containers/SidePanelContainer.less @@ -11,6 +11,7 @@ margin-bottom: 0 !important; font-size: 13px !important; font-weight: 600 !important; + background: linear-gradient(40deg, #6547ae, #5c499d) !important; } .panel-container { @@ -30,7 +31,6 @@ padding: 0; color: white; font-size: 1.5em; - margin-top: 5%; margin-left: auto; margin-right: auto; display: flex; @@ -45,10 +45,7 @@ .search-bar input { background-color: #5d4f8f; border-radius: 22px; - padding: 5%; - width: 80%; margin: auto; - height: 30px; border-color: #5d4f8f; font-size: 13px; } @@ -63,5 +60,16 @@ } .chat-outer-container { + height: calc(100% - 50px); overflow-x: hidden; + .chat-container { + height: inherit; + & > .row:first-child { + height: inherit; + } + } +} + +.messages-row { + height: calc(100% - 600px); } diff --git a/src/client/rooms/containers/SidePanelContainer.tsx b/src/client/rooms/containers/SidePanelContainer.tsx index f840f57b..8101344e 100644 --- a/src/client/rooms/containers/SidePanelContainer.tsx +++ b/src/client/rooms/containers/SidePanelContainer.tsx @@ -3,6 +3,7 @@ import { Container, Row } from "react-bootstrap"; import { MessageData, RoomType, UserDataResponseType } from "../../../types"; import { ChatContainer } from "../../chat"; import { Loader } from "../../components"; +import { isStaff } from "../../utils"; import { ParticipantsContainer } from "./ParticipantsContainer"; import "./SidePanelContainer.less"; @@ -28,18 +29,33 @@ export const SidePanelContainer: React.FunctionComponent = (
-
Tutor Team
+
Staff
+ {props.users ? ( + + isStaff(user.userType) + )} + raisedHandUsers={props.raisedHandUsers} + myUserId={props.myUserId} + /> + ) : ( + + + + )}
- +
Participants
{props.users ? ( !isStaff(user.userType) + )} raisedHandUsers={props.raisedHandUsers} myUserId={props.myUserId} /> @@ -50,7 +66,7 @@ export const SidePanelContainer: React.FunctionComponent = ( )}
- +
Chat
diff --git a/src/client/rooms/room.less b/src/client/rooms/room.less index 90ece1f0..e547cb58 100644 --- a/src/client/rooms/room.less +++ b/src/client/rooms/room.less @@ -1,7 +1,14 @@ +@import "../styles/Colours.less"; + +.classroom-container { + background-color: white; +} + .panel { + height: calc(100vh - 50px); .panel-container { width: 100%; - background-color: lightcyan; + background: linear-gradient(40deg, #8d78bf, #6860c8) !important; } .tutors-container { min-height: 200px; @@ -10,10 +17,111 @@ min-height: 400px; } .messages-container { + height: 100%; min-height: 200px; } } -.presenter-container { - height: 100%; - background-color: lightpink; + +.head-panel { + text-align: center; + align-items: center; +} + +.setting-icon { + font-size: 30px; + margin: 10px 10px 0; +} + +.classroom-btn-grp .back-btn { + background-color: @page-bg !important; + box-shadow: none; + color: grey; + font-weight: 600; + font-family: Raleway; + &:hover { + color: black !important; + box-shadow: none; + border: none !important; + } + &:active { + background-color: @page-bg !important; + color: grey !important; + box-shadow: none; + border: none !important; + } +} + +.first-btn { + border-radius: 15px 0 0 15px; + font-family: Raleway !important; + width: 10%; +} + +.setting-btn { + border-radius: 0; + width: 10%; +} + +.end-btn { + border-radius: 0 15px 15px 0; +} + +.first-btn, +.setting-btn, +.end-btn, +.file-btn { + color: white !important; + min-width: 100px; + padding: 5px; + height: 90px; + font-size: 10px; + font-family: Raleway !important; + font-weight: 600; + &:hover { + background-color: darken(grey, 10%) !important; + } + &:focus { + box-shadow: 0 0 0 0.2rem lighten(grey, 40%) !important; + color: @primary-dark !important; + } + &:active { + color: @primary-dark !important; + } +} + +.file-btn { + width: 100px; + color: #4f4f4f; + font-size: 10px; + font-family: "Raleway"; + font-weight: 600; + &:focus { + box-shadow: 0 0 0 0.2rem lighten(grey, 40%) !important; + color: #4f4f4f !important; + } +} + +.speaker-btn, +.participant-btn { + background-color: @page-bg !important; + color: #4f4f4f !important; + font-family: Raleway; + font-weight: 600; + align-content: center; + &:hover { + background-color: @content-bg !important; + } + &:focus { + background-color: @content-bg; + box-shadow: 0 0 0 0.2rem lighten(grey, 40%) !important; + } + &:active { + background-color: @content-bg !important; + color: #4f4f4f; + font-weight: 800; + } +} + +.participant-btn { + font-weight: 400 !important; } diff --git a/src/client/styles/App.less b/src/client/styles/App.less index 3fcace5b..83fc74e8 100644 --- a/src/client/styles/App.less +++ b/src/client/styles/App.less @@ -88,6 +88,10 @@ code { background: linear-gradient(40deg, #986eff, #7873f5) !important; } + .purple-gradient2 { + background: linear-gradient(40deg, #986eff, #7873f5) !important; + } + .peach-gradient { background: linear-gradient(40deg, #ff906f, #fc6262) !important; } diff --git a/src/client/videostreaming/RemotePeerVideo.tsx b/src/client/videostreaming/RemotePeerVideo.tsx index c5d59d49..47bc8320 100644 --- a/src/client/videostreaming/RemotePeerVideo.tsx +++ b/src/client/videostreaming/RemotePeerVideo.tsx @@ -38,5 +38,12 @@ export const RemotePeerVideo: React.FunctionComponent = ({ currentRef?.removeEventListener("loadedmetadata", playVideo); }, [peerId, myPeerId, addPeer, peerStreams]); - return