feat(ui): add recording request to ui side, add recording state and request window
This commit is contained in:
parent
d071d6d2cd
commit
249e731569
10 changed files with 170 additions and 40 deletions
|
|
@ -6,17 +6,19 @@ import { PlayerContext } from 'App/components/Session/playerContext';
|
|||
|
||||
interface Props {
|
||||
userDisplayName: string;
|
||||
type: WindowType;
|
||||
getWindowType: () => WindowType | null;
|
||||
}
|
||||
|
||||
export enum WindowType {
|
||||
Call,
|
||||
Control,
|
||||
Record,
|
||||
}
|
||||
|
||||
enum Actions {
|
||||
CallEnd,
|
||||
ControlEnd
|
||||
ControlEnd,
|
||||
RecordingEnd,
|
||||
}
|
||||
|
||||
const WIN_VARIANTS = {
|
||||
|
|
@ -24,27 +26,40 @@ const WIN_VARIANTS = {
|
|||
text: 'to accept the call',
|
||||
icon: 'call' as const,
|
||||
action: Actions.CallEnd,
|
||||
iconColor: 'teal',
|
||||
},
|
||||
[WindowType.Control]: {
|
||||
text: 'to accept remote control request',
|
||||
icon: 'remote-control' as const,
|
||||
action: Actions.ControlEnd,
|
||||
iconColor: 'teal',
|
||||
},
|
||||
[WindowType.Record]: {
|
||||
text: 'to accept recording request',
|
||||
icon: 'record-circle' as const,
|
||||
iconColor: 'red',
|
||||
action: Actions.RecordingEnd,
|
||||
}
|
||||
};
|
||||
|
||||
function RequestingWindow({ userDisplayName, type }: Props) {
|
||||
function RequestingWindow({ userDisplayName, getWindowType }: Props) {
|
||||
const windowType = getWindowType()
|
||||
if (!windowType) return;
|
||||
const { player } = React.useContext(PlayerContext)
|
||||
|
||||
|
||||
const {
|
||||
assistManager: {
|
||||
initiateCallEnd,
|
||||
releaseRemoteControl,
|
||||
stopRecording,
|
||||
}
|
||||
} = player
|
||||
|
||||
const actions = {
|
||||
[Actions.CallEnd]: initiateCallEnd,
|
||||
[Actions.ControlEnd]: releaseRemoteControl
|
||||
[Actions.ControlEnd]: releaseRemoteControl,
|
||||
[Actions.RecordingEnd]: stopRecording,
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
|
@ -52,13 +67,13 @@ function RequestingWindow({ userDisplayName, type }: Props) {
|
|||
style={{ background: 'rgba(0,0,0, 0.30)', zIndex: INDEXES.PLAYER_REQUEST_WINDOW }}
|
||||
>
|
||||
<div className="rounded bg-white pt-4 pb-2 px-8 flex flex-col text-lg items-center max-w-lg text-center">
|
||||
<Icon size={40} color="teal" name={WIN_VARIANTS[type].icon} className="mb-4" />
|
||||
<Icon size={40} color={WIN_VARIANTS[windowType].iconColor} name={WIN_VARIANTS[windowType].icon} className="mb-4" />
|
||||
<div>
|
||||
Waiting for <span className="font-semibold">{userDisplayName}</span>
|
||||
</div>
|
||||
<span>{WIN_VARIANTS[type].text}</span>
|
||||
<span>{WIN_VARIANTS[windowType].text}</span>
|
||||
<Loader size={30} style={{ minHeight: 60 }} />
|
||||
<Button variant="text-primary" onClick={actions[WIN_VARIANTS[type].action]}>
|
||||
<Button variant="text-primary" onClick={actions[WIN_VARIANTS[windowType].action]}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { toast } from 'react-toastify';
|
||||
import { confirm } from 'UI';
|
||||
import stl from './AassistActions.module.css';
|
||||
import ScreenRecorder from 'App/components/Session_/ScreenRecorder/ScreenRecorder';
|
||||
|
||||
function onReject() {
|
||||
toast.info(`Call was rejected.`);
|
||||
|
|
@ -184,6 +185,10 @@ function AssistActions({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* @ts-ignore wtf? */}
|
||||
<ScreenRecorder />
|
||||
<div className={stl.divider} />
|
||||
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Go live to initiate remote control" disabled={livePlay}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import React from 'react';
|
||||
import { CallingState, ConnectionStatus, RemoteControlStatus, getStatusText } from 'Player';
|
||||
import {
|
||||
SessionRecordingStatus,
|
||||
getStatusText,
|
||||
CallingState,
|
||||
ConnectionStatus,
|
||||
RemoteControlStatus,
|
||||
} from 'Player';
|
||||
|
||||
import AutoplayTimer from './Overlay/AutoplayTimer';
|
||||
import PlayIconLayer from './Overlay/PlayIconLayer';
|
||||
|
|
@ -36,6 +42,7 @@ function Overlay({
|
|||
livePlay,
|
||||
calling,
|
||||
remoteControl,
|
||||
recordingState,
|
||||
} = store.get()
|
||||
const loading = messagesLoading || cssLoading
|
||||
const liveStatusText = getStatusText(peerConnectionStatus)
|
||||
|
|
@ -45,25 +52,41 @@ function Overlay({
|
|||
const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
|
||||
const showLiveStatusText = live && livePlay && liveStatusText && !loading;
|
||||
|
||||
const showRequestWindow = live && (calling === CallingState.Connecting || remoteControl === RemoteControlStatus.Requesting)
|
||||
const requestWindowType = calling === CallingState.Connecting ? WindowType.Call : remoteControl === RemoteControlStatus.Requesting ? WindowType.Control : null
|
||||
const showRequestWindow =
|
||||
live &&
|
||||
(calling === CallingState.Connecting ||
|
||||
remoteControl === RemoteControlStatus.Requesting ||
|
||||
recordingState === SessionRecordingStatus.Requesting);
|
||||
|
||||
const getRequestWindowType = () => {
|
||||
if (calling === CallingState.Connecting) {
|
||||
return WindowType.Call
|
||||
}
|
||||
if (remoteControl === RemoteControlStatus.Requesting) {
|
||||
return WindowType.Control
|
||||
}
|
||||
if (recordingState === SessionRecordingStatus.Requesting) {
|
||||
return WindowType.Record
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showRequestWindow ? <RequestingWindow type={requestWindowType} /> : null}
|
||||
{ showAutoplayTimer && <AutoplayTimer /> }
|
||||
{ showLiveStatusText &&
|
||||
<LiveStatusText text={liveStatusText} concetionStatus={closedLive ? ConnectionStatus.Closed : concetionStatus} />
|
||||
}
|
||||
{ loading ? <Loader /> : null }
|
||||
{ showPlayIconLayer &&
|
||||
<PlayIconLayer playing={playing} togglePlay={togglePlay} />
|
||||
}
|
||||
{ markedTargets && <ElementsMarker targets={ markedTargets } activeIndex={activeTargetIndex}/>
|
||||
}
|
||||
{showRequestWindow ? <RequestingWindow getWindowType={getRequestWindowType} /> : null}
|
||||
{showAutoplayTimer && <AutoplayTimer />}
|
||||
{showLiveStatusText && (
|
||||
<LiveStatusText
|
||||
text={liveStatusText}
|
||||
concetionStatus={closedLive ? ConnectionStatus.Closed : concetionStatus}
|
||||
/>
|
||||
)}
|
||||
{loading ? <Loader /> : null}
|
||||
{showPlayIconLayer && <PlayIconLayer playing={playing} togglePlay={togglePlay} />}
|
||||
{markedTargets && <ElementsMarker targets={markedTargets} activeIndex={activeTargetIndex} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(Overlay);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import React from 'react';
|
||||
import { screenRecorder } from 'App/utils/screenRecorder';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
|
||||
import { Button } from 'UI'
|
||||
import { requestRecording, stopRecording, connectPlayer } from 'Player'
|
||||
import {
|
||||
SessionRecordingStatus,
|
||||
} from 'Player/MessageDistributor/managers/AssistManager';
|
||||
let stopRecorderCb: () => void
|
||||
|
||||
/**
|
||||
|
|
@ -22,35 +26,60 @@ function isSupported() {
|
|||
return false
|
||||
}
|
||||
|
||||
function ScreenRecorder() {
|
||||
const supportedBrowsers = ["Chrome v91+", "Edge v90+"]
|
||||
const supportedMessage = `Supported Browsers: ${supportedBrowsers.join(', ')}`
|
||||
|
||||
function ScreenRecorder({ recordingState }: { recordingState: SessionRecordingStatus }) {
|
||||
const [isRecording, setRecording] = React.useState(false);
|
||||
|
||||
const toggleRecording = async () => {
|
||||
console.log(isRecording);
|
||||
if (isRecording) {
|
||||
stopRecorderCb?.();
|
||||
setRecording(false);
|
||||
} else {
|
||||
React.useEffect(() => {
|
||||
return () => stopRecorderCb?.()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRecording && recordingState === SessionRecordingStatus.Recording) {
|
||||
startRecording();
|
||||
}
|
||||
if (isRecording && recordingState !== SessionRecordingStatus.Recording) {
|
||||
stopRecordingHandler();
|
||||
}
|
||||
}, [recordingState, isRecording])
|
||||
|
||||
const startRecording = async () => {
|
||||
const stop = await screenRecorder();
|
||||
stopRecorderCb = stop;
|
||||
setRecording(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isSupportedBrowser = isSupported()
|
||||
if (!isSupportedBrowser) return (
|
||||
<div className="p-3">
|
||||
const stopRecordingHandler = () => {
|
||||
stopRecording()
|
||||
stopRecorderCb?.();
|
||||
setRecording(false);
|
||||
}
|
||||
|
||||
const recordingRequest = () => {
|
||||
requestRecording()
|
||||
// startRecording()
|
||||
}
|
||||
|
||||
if (!isSupported()) return (
|
||||
<div className="p-2">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Supported browsers: Chrome v91+; Edge v90+">
|
||||
<div className="p-1 text-disabled-text cursor-not-allowed">Record</div>
|
||||
<Tooltip title={supportedMessage}>
|
||||
<Button icon="record-circle" disabled variant={isRecording ? "text-red" : "text-primary"}>
|
||||
Record Activity
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div onClick={toggleRecording} className="p-3">
|
||||
<div className="p-1 font-semibold cursor-pointer hover:text-main">{isRecording ? 'STOP' : 'RECORD'}</div>
|
||||
<div onClick={!isRecording ? recordingRequest : stopRecordingHandler} className="p-2">
|
||||
<Button icon={!isRecording ? 'stop-record-circle' : 'record-circle'} variant={isRecording ? "text-red" : "text-primary"}>
|
||||
{isRecording ? 'Stop Recording' : 'Record Activity'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScreenRecorder
|
||||
// @ts-ignore
|
||||
export default connectPlayer(state => ({ recordingState: state.recordingState}))(ScreenRecorder)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -111,6 +111,11 @@ export default class Screen {
|
|||
return Object.assign(this.screen.style, styles)
|
||||
}
|
||||
|
||||
toggleRecordingStatus(isEnabled: boolean) {
|
||||
const styles = isEnabled ? { border: '2px dashed red' } : { border: 'unset'}
|
||||
return Object.assign(this.screen.style, styles)
|
||||
}
|
||||
|
||||
get window(): WindowProxy | null {
|
||||
return this.iframe.contentWindow;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ export enum RemoteControlStatus {
|
|||
Enabled,
|
||||
}
|
||||
|
||||
export enum SessionRecordingStatus {
|
||||
Off,
|
||||
Requesting,
|
||||
Recording
|
||||
}
|
||||
|
||||
|
||||
export function getStatusText(status: ConnectionStatus): string {
|
||||
switch(status) {
|
||||
|
|
@ -58,6 +64,7 @@ export interface State {
|
|||
calling: CallingState;
|
||||
peerConnectionStatus: ConnectionStatus;
|
||||
remoteControl: RemoteControlStatus;
|
||||
recordingState: SessionRecordingStatus;
|
||||
annotating: boolean;
|
||||
assistStart: number;
|
||||
}
|
||||
|
|
@ -66,6 +73,7 @@ export const INITIAL_STATE: State = {
|
|||
calling: CallingState.NoCall,
|
||||
peerConnectionStatus: ConnectionStatus.Connecting,
|
||||
remoteControl: RemoteControlStatus.Disabled,
|
||||
recordingState: SessionRecordingStatus.Off,
|
||||
annotating: false,
|
||||
assistStart: 0,
|
||||
}
|
||||
|
|
@ -245,12 +253,45 @@ export default class AssistManager {
|
|||
this.toggleRemoteControl(false)
|
||||
})
|
||||
socket.on('call_end', this.onRemoteCallEnd)
|
||||
socket.on('recording_accepted', () => {
|
||||
this.toggleRecording(true)
|
||||
})
|
||||
socket.on('recording_denied', () => {
|
||||
this.toggleRecording(false)
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', this.onVisChange)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/* ==== Recording the session ==== */
|
||||
|
||||
public requestRecording = () => {
|
||||
const recordingState = getState().recordingState
|
||||
if (!this.socket || recordingState === SessionRecordingStatus.Requesting) return;
|
||||
|
||||
update({ recordingState: SessionRecordingStatus.Requesting })
|
||||
this.socket.emit("request_recording", JSON.stringify({
|
||||
...this.session.agentInfo,
|
||||
query: document.location.search,
|
||||
}))
|
||||
}
|
||||
|
||||
public stopRecording = () => {
|
||||
const recordingState = getState().recordingState
|
||||
if (!this.socket || recordingState === SessionRecordingStatus.Off) return;
|
||||
|
||||
this.socket.emit("stop_recording")
|
||||
this.toggleRecording(false)
|
||||
}
|
||||
|
||||
private toggleRecording = (isAccepted: boolean) => {
|
||||
this.md.toggleRecordingStatus(isAccepted)
|
||||
|
||||
update({ recordingStatus: isAccepted ? SessionRecordingStatus.Recording : SessionRecordingStatus.Off })
|
||||
}
|
||||
|
||||
/* ==== Remote Control ==== */
|
||||
|
||||
private onMouseMove = (e: MouseEvent): void => {
|
||||
|
|
|
|||
4
frontend/app/svg/icons/record-circle.svg
Normal file
4
frontend/app/svg/icons/record-circle.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
3
frontend/app/svg/icons/stop-record-circle.svg
Normal file
3
frontend/app/svg/icons/stop-record-circle.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path clip-rule="evenoddCustomFill" d="M8 16H16V8H8V16ZM12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
|
|
@ -90,6 +90,9 @@ ${icons.map(icon => {
|
|||
.replace(/xmlns\:xlink/g, 'xmlnsXlink')
|
||||
.replace(/clip-path/g, 'clipPath')
|
||||
.replace(/clip-rule/g, 'clipRule')
|
||||
// hack to keep fill rule for some icons like stop recording square
|
||||
.replace(/clipRule="evenoddCustomFill"/g, 'clipRule="evenodd" fillRule="evenodd"')
|
||||
.replace(/fill-rule/g, 'fillRule')
|
||||
.replace(/fill-opacity/g, 'fillOpacity')
|
||||
.replace(/stop-color/g, 'stopColor')
|
||||
.replace(/xml:space="preserve"/g, '')};`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue