diff --git a/app/livechat/server/api/v1/videoCall.js b/app/livechat/server/api/v1/videoCall.js index db11ebddbdaa9..ba1ebd31cc9b5 100644 --- a/app/livechat/server/api/v1/videoCall.js +++ b/app/livechat/server/api/v1/videoCall.js @@ -94,7 +94,7 @@ API.v1.addRoute('livechat/webrtc.call', { authRequired: true }, { if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { callStatus = 'ringing'; - Promise.await(Rooms.setCallStatus(room._id, callStatus)); + Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus)); Promise.await(Messages.createWithTypeRoomIdMessageAndUser( 'livechat_webrtc_video_call', room._id, diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js index 4bc99ce43d4c7..d39f439fdca80 100644 --- a/app/models/server/models/Rooms.js +++ b/app/models/server/models/Rooms.js @@ -73,6 +73,21 @@ export class Rooms extends Base { return this.update(query, update); } + setCallStatusAndCallStartTime(_id, status) { + const query = { + _id, + }; + + const update = { + $set: { + callStatus: status, + webRtcCallStartTime: new Date(), + }, + }; + + return this.update(query, update); + } + findByTokenpass(tokens) { const query = { 'tokenpass.tokens.token': { diff --git a/client/views/meet/CallPage.css b/client/views/meet/CallPage.css index d206ea0f137bd..799fd59faec80 100644 --- a/client/views/meet/CallPage.css +++ b/client/views/meet/CallPage.css @@ -11,24 +11,31 @@ } .Self_Video { + display: flex; + width: 15%; + height: 17.5%; + + justify-content: center; } -@media (max-width: 1200px) { +@media (max-width: 900px) and (min-height: 500px) { .Self_Video { - width: 20%; + width: 30%; + height: 20%; } } -@media (max-width: 900px) { +@media (max-width: 900px) and (max-height: 500px) { .Self_Video { width: 30%; + height: 35%; } } -@media (max-width: 900px) and (min-height: 500px) { +@media (min-width: 901px) and (max-width: 1300px) and (max-height: 500px) { .Self_Video { - width: 30%; - height: 20%; + width: 20%; + height: 40%; } } diff --git a/client/views/meet/CallPage.js b/client/views/meet/CallPage.js index 11b038fa9d673..ecc8ea8d85cf7 100644 --- a/client/views/meet/CallPage.js +++ b/client/views/meet/CallPage.js @@ -1,23 +1,52 @@ import { Box, Flex, ButtonGroup, Button, Icon } from '@rocket.chat/fuselage'; +import moment from 'moment'; import React, { useEffect, useState } from 'react'; import { Notifications } from '../../../app/notifications/client'; import { WebRTC } from '../../../app/webrtc/client'; import { WEB_RTC_EVENTS } from '../../../app/webrtc/index'; +import UserAvatar from '../../components/avatar/UserAvatar'; import { useTranslation } from '../../contexts/TranslationContext'; +import OngoingCallDuration from './OngoingCallDuration'; import './CallPage.css'; -function CallPage({ roomId, visitorToken, visitorId, status, setStatus, layout }) { +function CallPage({ + roomId, + visitorToken, + visitorId, + status, + setStatus, + layout, + visitorName, + agentName, + callStartTime, +}) { const [isAgentActive, setIsAgentActive] = useState(false); const [isMicOn, setIsMicOn] = useState(false); const [isCameraOn, setIsCameraOn] = useState(false); const [isRemoteMobileDevice, setIsRemoteMobileDevice] = useState(false); + const [callInIframe, setCallInIframe] = useState(false); + const [isRemoteCameraOn, setIsRemoteCameraOn] = useState(false); + const [isLocalMobileDevice, setIsLocalMobileDevice] = useState(false); + + let iconSize = 'x21'; + let buttonSize = 'x40'; + const avatarSize = 'x48'; + if (layout === 'embedded') { + iconSize = 'x19'; + buttonSize = 'x35'; + } + const t = useTranslation(); useEffect(() => { if (visitorToken) { const webrtcInstance = WebRTC.getInstanceByRoomId(roomId, visitorId); const isMobileDevice = () => { - if (window.innerWidth <= 450 && window.innerHeight >= 620) { + if (layout === 'embedded') { + setCallInIframe(true); + } + if (window.innerWidth <= 450 && window.innerHeight >= 629 && window.innerHeight <= 900) { + setIsLocalMobileDevice(true); webrtcInstance.media = { audio: true, video: { @@ -47,6 +76,8 @@ function CallPage({ roomId, visitorToken, visitorId, status, setStatus, layout } Notifications.notifyRoom(roomId, 'webrtc', 'deviceType', { isMobileDevice: isMobileDevice(), }); + } else if (type === 'cameraStatus') { + setIsRemoteCameraOn(data.isCameraOn); } }); Notifications.notifyRoom(roomId, 'webrtc', 'deviceType', { @@ -83,11 +114,13 @@ function CallPage({ roomId, visitorToken, visitorId, status, setStatus, layout } setStatus(data.callStatus); } else if (type === 'deviceType' && data.isMobileDevice) { setIsRemoteMobileDevice(true); + } else if (type === 'cameraStatus') { + setIsRemoteCameraOn(data.isCameraOn); } }); setIsAgentActive(true); } - }, [isAgentActive, status, setStatus, visitorId, roomId, visitorToken]); + }, [isAgentActive, status, setStatus, visitorId, roomId, visitorToken, layout]); const toggleButton = (control) => { if (control === 'mic') { @@ -96,6 +129,7 @@ function CallPage({ roomId, visitorToken, visitorId, status, setStatus, layout } } WebRTC.getInstanceByRoomId(roomId, visitorToken).toggleVideo(); setIsCameraOn(!isCameraOn); + Notifications.notifyRoom(roomId, 'webrtc', 'cameraStatus', { isCameraOn: !isCameraOn }); }; const closeWindow = () => { @@ -105,31 +139,159 @@ function CallPage({ roomId, visitorToken, visitorId, status, setStatus, layout } return window.close(); }; - const iconSize = layout === 'embedded' ? 'x19' : 'x21'; - const buttonSize = layout === 'embedded' ? 'x35' : 'x40'; + const getCallDuration = (callStartTime) => + moment.duration(moment(new Date()).diff(moment(callStartTime))).asSeconds(); - switch (status) { - case 'ringing': - // Todo Deepak - return ( -

- Waiting for the visitor to join ... -

- ); - case 'declined': - return ( + const showCallPage = (localAvatar, remoteAvatar) => ( + + + + + + + + + {layout === 'embedded' && ( + + )} + + + + - {t('Call_declined')} + + + + + + {remoteAvatar} + - ); - case 'inProgress': + + + ); + + switch (status) { + case 'ringing': return ( - + username={agentName} + className='rcx-message__avatar' + size={isLocalMobileDevice ? 'x32' : 'x48'} + /> - - - - {layout === 'embedded' && ( - - )} - - - + {visitorName} + + ); + + case 'declined': + return ( + + {t('Call_declined')} + + ); + case 'inProgress': + return ( + + {visitorToken + ? showCallPage(visitorName, agentName) + : showCallPage(agentName, visitorName)} + + ); } } diff --git a/client/views/meet/MeetPage.js b/client/views/meet/MeetPage.js index eb166b7c31d95..6985889dd6348 100644 --- a/client/views/meet/MeetPage.js +++ b/client/views/meet/MeetPage.js @@ -1,11 +1,14 @@ +import { Button, Box, Icon, Flex } from '@rocket.chat/fuselage'; import { Meteor } from 'meteor/meteor'; import React, { useEffect, useState, useCallback } from 'react'; import { APIClient } from '../../../app/utils/client'; +import UserAvatar from '../../components/avatar/UserAvatar'; import { useRouteParameter, useQueryStringParameter } from '../../contexts/RouterContext'; import NotFoundPage from '../notFound/NotFoundPage'; import PageLoading from '../root/PageLoading'; import CallPage from './CallPage'; +import './CallPage.css'; function MeetPage() { const [isRoomMember, setIsRoomMember] = useState(false); @@ -14,12 +17,22 @@ function MeetPage() { const roomId = useRouteParameter('rid'); const visitorToken = useQueryStringParameter('token'); const layout = useQueryStringParameter('layout'); + const [visitorName, setVisitorName] = useState(''); + const [agentName, setAgentName] = useState(''); + const [callStartTime, setCallStartTime] = useState(undefined); + + const isMobileDevice = () => window.innerWidth <= 450; const setupCallForVisitor = useCallback(async () => { const room = await APIClient.v1.get(`/livechat/room?token=${visitorToken}&rid=${roomId}`); if (room?.room?.v?.token === visitorToken) { setVisitorId(room.room.v._id); + setVisitorName(room.room.fname); + room?.room?.responseBy?.username + ? setAgentName(room.room.responseBy.username) + : setAgentName(room.room.servedBy.username); setStatus(room?.room?.callStatus || 'ended'); + setCallStartTime(room.room.webRtcCallStartTime); return setIsRoomMember(true); } }, [visitorToken, roomId]); @@ -27,7 +40,12 @@ function MeetPage() { const setupCallForAgent = useCallback(async () => { const room = await APIClient.v1.get(`/rooms.info?roomId=${roomId}`); if (room?.room?.servedBy?._id === Meteor.userId()) { + setVisitorName(room.room.fname); + room?.room?.responseBy?.username + ? setAgentName(room.room.responseBy.username) + : setAgentName(room.room.servedBy.username); setStatus(room?.room?.callStatus || 'ended'); + setCallStartTime(room.room.webRtcCallStartTime); return setIsRoomMember(true); } }, [roomId]); @@ -44,9 +62,86 @@ function MeetPage() { if (!isRoomMember) { return ; } + const closeCallTab = () => { + window.close(); + }; if (status === 'ended') { - return

Ended!

; + return ( + + + + + + + +

{'Call Ended!'}

+

+ {visitorToken ? agentName : visitorName} +

+
+ + + +
+
+ ); } + return ( + callStartTime={callStartTime} + /> ); } diff --git a/client/views/meet/OngoingCallDuration.tsx b/client/views/meet/OngoingCallDuration.tsx new file mode 100644 index 0000000000000..e3a60a123a46c --- /dev/null +++ b/client/views/meet/OngoingCallDuration.tsx @@ -0,0 +1,22 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC, useEffect, useState } from 'react'; + +type OngoingCallDurationProps = { + counter: number; + fontSize: number; +}; + +const OngoingCallDuration: FC = ({ counter: defaultCounter = 0 }) => { + const [counter, setCounter] = useState(defaultCounter); + useEffect(() => { + setTimeout(() => setCounter(counter + 1), 1000); + }, [counter]); + + return ( + + {new Date(counter * 1000).toISOString().substr(11, 8)} + + ); +}; + +export default OngoingCallDuration; diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 81112bc872b63..b7c0d3867967c 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -34,6 +34,7 @@ export interface IRoom extends IRocketChatRecord { usersCount: number; jitsiTimeout: Date; callStatus?: CallStatus; + webRtcCallStartTime?: Date; servedBy?: { _id: string; };