From c56a0da63b286c8d4425c6106db134d030934174 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 15 Sep 2022 16:55:27 +0200 Subject: [PATCH] change(ui): change cursor icon for assist, add usernames to cursors for user and agent --- .../AssistActions/AssistActions.tsx | 375 ++++++++++-------- .../StatedScreen/Screen/Cursor.ts | 29 +- .../StatedScreen/Screen/cursor.module.css | 4 +- frontend/app/player/Player.ts | 6 +- frontend/app/player/singletone.js | 1 + .../tracker-assist/src/AnnotationCanvas.ts | 4 +- tracker/tracker-assist/src/Assist.ts | 10 +- tracker/tracker-assist/src/Mouse.ts | 35 +- tracker/tracker-assist/src/RemoteControl.ts | 25 +- 9 files changed, 292 insertions(+), 197 deletions(-) diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 58773acaf..cc54da5bb 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -5,8 +5,18 @@ import cn from 'classnames'; import { toggleChatWindow } from 'Duck/sessions'; import { connectPlayer } from 'Player/store'; import ChatWindow from '../../ChatWindow'; -import { callPeer, setCallArgs, requestReleaseRemoteControl, toggleAnnotation } from 'Player'; -import { CallingState, ConnectionStatus, RemoteControlStatus } from 'Player/MessageDistributor/managers/AssistManager'; +import { + callPeer, + setCallArgs, + requestReleaseRemoteControl, + toggleAnnotation, + toggleUserName, +} from 'Player'; +import { + CallingState, + ConnectionStatus, + RemoteControlStatus, +} from 'Player/MessageDistributor/managers/AssistManager'; import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream'; import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream'; @@ -15,199 +25,228 @@ import { confirm } from 'UI'; import stl from './AassistActions.module.css'; function onReject() { - toast.info(`Call was rejected.`); + toast.info(`Call was rejected.`); } function onError(e: any) { - console.log(e) - toast.error(typeof e === 'string' ? e : e.message); + console.log(e); + toast.error(typeof e === 'string' ? e : e.message); } interface Props { - userId: string; - calling: CallingState; - annotating: boolean; - peerConnectionStatus: ConnectionStatus; - remoteControlStatus: RemoteControlStatus; - hasPermission: boolean; - isEnterprise: boolean; - isCallActive: boolean; - agentIds: string[]; - livePlay: boolean; + userId: string; + calling: CallingState; + annotating: boolean; + peerConnectionStatus: ConnectionStatus; + remoteControlStatus: RemoteControlStatus; + hasPermission: boolean; + isEnterprise: boolean; + isCallActive: boolean; + agentIds: string[]; + livePlay: boolean; + userDisplayName: string; } function AssistActions({ - userId, - calling, - annotating, - peerConnectionStatus, - remoteControlStatus, - hasPermission, - isEnterprise, - isCallActive, - agentIds, - livePlay + userId, + calling, + annotating, + peerConnectionStatus, + remoteControlStatus, + hasPermission, + isEnterprise, + isCallActive, + agentIds, + livePlay, + userDisplayName, }: Props) { - const [isPrestart, setPrestart] = useState(false); - const [incomeStream, setIncomeStream] = useState([]); - const [localStream, setLocalStream] = useState(null); - const [callObject, setCallObject] = useState<{ end: () => void } | null>(null); + const [isPrestart, setPrestart] = useState(false); + const [incomeStream, setIncomeStream] = useState([]); + const [localStream, setLocalStream] = useState(null); + const [callObject, setCallObject] = useState<{ end: () => void } | null>(null); - const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting; - const callRequesting = calling === CallingState.Connecting - const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission); - const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled; + const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting; + const callRequesting = calling === CallingState.Connecting; + const cannotCall = + peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission); + const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled; - - useEffect(() => { - return callObject?.end() - }, []) - - useEffect(() => { - if (peerConnectionStatus == ConnectionStatus.Disconnected) { - toast.info(`Live session was closed.`); - } - }, [peerConnectionStatus]); - - const addIncomeStream = (stream: MediaStream) => { - setIncomeStream(oldState => { - if (!oldState.find(existingStream => existingStream.id === stream.id)) { - return [...oldState, stream] - } - return oldState - }); + useEffect(() => { + if (!onCall && isCallActive && agentIds) { + setPrestart(true); + // call(agentIds); do not autocall on prestart, can change later } + }, [agentIds, isCallActive]); - function call(additionalAgentIds?: string[]) { - RequestLocalStream().then(lStream => { - setLocalStream(lStream); - setCallArgs( - lStream, - addIncomeStream, - lStream.stop.bind(lStream), - onReject, - onError - ) - setCallObject(callPeer()); - if (additionalAgentIds) { - callPeer(additionalAgentIds) - } - }).catch(onError) + useEffect(() => { + if (!livePlay) { + if (annotating) { + toggleAnnotation(false); + } + if (remoteActive) { + requestReleaseRemoteControl(); + } } + }, [livePlay]); - React.useEffect(() => { - if (!onCall && isCallActive && agentIds) { - setPrestart(true); - // call(agentIds); do not autocall on prestart, can change later - } - }, [agentIds, isCallActive]) - - const confirmCall = async () => { - if (callRequesting) return; - - if ( - await confirm({ - header: 'Start Call', - confirmButton: 'Call', - confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`, - }) - ) { - call(agentIds); - } - }; - - const requestControl = () => { - if (callRequesting) return; - requestReleaseRemoteControl() + useEffect(() => { + if (remoteActive) { + toggleUserName(userDisplayName); + } else { + toggleUserName(); } + }, [remoteActive]); - React.useEffect(() => { - if (!livePlay) { - if (annotating) { - toggleAnnotation(false); - } - if (remoteActive) { - requestReleaseRemoteControl() - } + useEffect(() => { + return callObject?.end(); + }, []); + + useEffect(() => { + if (peerConnectionStatus == ConnectionStatus.Disconnected) { + toast.info(`Live session was closed.`); + } + }, [peerConnectionStatus]); + + const addIncomeStream = (stream: MediaStream) => { + setIncomeStream((oldState) => { + if (!oldState.find((existingStream) => existingStream.id === stream.id)) { + return [...oldState, stream]; + } + return oldState; + }); + }; + + function call(additionalAgentIds?: string[]) { + RequestLocalStream() + .then((lStream) => { + setLocalStream(lStream); + setCallArgs(lStream, addIncomeStream, lStream.stop.bind(lStream), onReject, onError); + setCallObject(callPeer()); + if (additionalAgentIds) { + callPeer(additionalAgentIds); } - }, [livePlay]) + }) + .catch(onError); + } - return ( -
- {(onCall || remoteActive) && ( - <> -
toggleAnnotation(!annotating)} - role="button" - > - - {/* */} -
-
- - )} -
{ + if (callRequesting) return; + + if ( + await confirm({ + header: 'Start Call', + confirmButton: 'Call', + confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`, + }) + ) { + call(agentIds); + } + }; + + const requestControl = () => { + if (callRequesting) return; + requestReleaseRemoteControl(); + }; + + return ( +
+ {(onCall || remoteActive) && ( + <> +
toggleAnnotation(!annotating)} + role="button" + > + - {/* */} -
-
+ Annotate + + {/* */} +
+
+ + )} +
+ + {/* */} +
+
- -
- - {/* */} -
-
- -
- {onCall && callObject && ( - - )} -
+ +
+ + {/* */}
- ); +
+ +
+ {onCall && callObject && ( + + )} +
+
+ ); } const con = connect( - (state) => { - const permissions = state.getIn(['user', 'account', 'permissions']) || []; - return { - hasPermission: permissions.includes('ASSIST_CALL'), - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', - }; - }, - { toggleChatWindow } + (state) => { + const permissions = state.getIn(['user', 'account', 'permissions']) || []; + return { + hasPermission: permissions.includes('ASSIST_CALL'), + isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', + userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']), + }; + }, + { toggleChatWindow } ); export default con( - connectPlayer((state) => ({ - calling: state.calling, - annotating: state.annotating, - remoteControlStatus: state.remoteControl, - peerConnectionStatus: state.peerConnectionStatus, - livePlay: state.livePlay, - }))(AssistActions) + connectPlayer((state) => ({ + calling: state.calling, + annotating: state.annotating, + remoteControlStatus: state.remoteControl, + peerConnectionStatus: state.peerConnectionStatus, + livePlay: state.livePlay, + }))(AssistActions) ); diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts index cd583c05b..d0b6e5d00 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts @@ -4,6 +4,7 @@ import styles from './cursor.module.css'; export default class Cursor { private readonly cursor: HTMLDivElement; + private nameElement: HTMLDivElement; private readonly position: Point = { x: -1, y: -1 } constructor(overlay: HTMLDivElement) { this.cursor = document.createElement('div'); @@ -19,6 +20,32 @@ export default class Cursor { } } + toggleUserName(name?: string) { + if (!this.nameElement) { + this.nameElement = document.createElement('div') + Object.assign(this.nameElement.style, { + position: 'absolute', + padding: '4px 6px', + borderRadius: '8px', + backgroundColor: 'rgb(57, 78, 255)', + color: 'white', + bottom: '-30px', + left: '100%', + fontSize: '16px', + whiteSpace: 'nowrap', + }) + this.cursor.appendChild(this.nameElement) + } + + if (!name) { + this.nameElement.style.display = 'none' + } else { + this.nameElement.style.display = 'block' + const nameStr = name ? name.length > 10 ? name.slice(0, 9) + '...' : name : 'User' + this.nameElement.innerHTML = `${nameStr}` + } + } + move({ x, y }: Point) { this.position.x = x; this.position.y = y; @@ -41,4 +68,4 @@ export default class Cursor { return { x: this.position.x, y: this.position.y }; } -} \ No newline at end of file +} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css b/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css index f6ffc1852..7a94c99b8 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css @@ -1,7 +1,7 @@ .cursor { display: block; position: absolute; - width: 20px; + width: 13px; height: 20px; background-image: url('data:image/svg+xml;utf8,'); background-repeat: no-repeat; @@ -12,7 +12,7 @@ } /* ====== * - Source: https://github.com/codrops/ClickEffects/blob/master/css/component.css + Source: https://github.com/codrops/ClickEffects/blob/master/css/component.css * ======= */ .cursor::after { position: absolute; diff --git a/frontend/app/player/Player.ts b/frontend/app/player/Player.ts index ee9814275..db1a20dde 100644 --- a/frontend/app/player/Player.ts +++ b/frontend/app/player/Player.ts @@ -263,7 +263,11 @@ export default class Player extends MessageDistributor { this._setTime(getState().endTime); this._startAnimation(); update({ livePlay: true }); -} + } + + toggleUserName(name?: string) { + this.cursor.toggleUserName(name) + } clean() { this.pause(); diff --git a/frontend/app/player/singletone.js b/frontend/app/player/singletone.js index 2d3d2255d..0964f84e5 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/singletone.js @@ -82,6 +82,7 @@ export const toggleAnnotation = initCheck((...args) => instance.assistManager.to /** @type {Player.toggleTimetravel} */ export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args)) export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args)) +export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args)) export const Controls = { jump, diff --git a/tracker/tracker-assist/src/AnnotationCanvas.ts b/tracker/tracker-assist/src/AnnotationCanvas.ts index 1341045f6..bc7fc01d9 100644 --- a/tracker/tracker-assist/src/AnnotationCanvas.ts +++ b/tracker/tracker-assist/src/AnnotationCanvas.ts @@ -13,7 +13,7 @@ export default class AnnotationCanvas { }) } - private resizeCanvas = () => { + private readonly resizeCanvas = () => { this.canvas.width = window.innerWidth this.canvas.height = window.innerHeight } @@ -78,4 +78,4 @@ export default class AnnotationCanvas { } window.removeEventListener('resize', this.resizeCanvas) } -} \ No newline at end of file +} diff --git a/tracker/tracker-assist/src/Assist.ts b/tracker/tracker-assist/src/Assist.ts index 02ecc50e0..ba6867870 100644 --- a/tracker/tracker-assist/src/Assist.ts +++ b/tracker/tracker-assist/src/Assist.ts @@ -156,6 +156,7 @@ export default class Assist { this.emit('control_granted', id) annot = new AnnotationCanvas() annot.mount() + return callingAgents.get(id) }, id => { const cb = this.agents[id].onControlReleased @@ -211,7 +212,7 @@ export default class Assist { }) socket.on('AGENT_DISCONNECTED', (id) => { - remoteControl.releaseControl(id) + remoteControl.releaseControl() this.agents[id]?.onDisconnect?.() delete this.agents[id] @@ -298,7 +299,9 @@ export default class Assist { const handleCallEnd = () => { // Completle stop and clear all calls // Streams Object.values(calls).forEach(call => call.close()) - Object.keys(calls).forEach(peerId => delete calls[peerId]) + Object.keys(calls).forEach(peerId => { + delete calls[peerId] + }) Object.values(lStreams).forEach((stream) => { stream.stop() }) Object.keys(lStreams).forEach((peerId: string) => { delete lStreams[peerId] }) @@ -312,6 +315,9 @@ export default class Assist { this.emit('UPDATE_SESSION', { agentIds: [], isCallActive: false, }) this.setCallingState(CallingState.False) sessionStorage.removeItem(this.options.session_calling_peer_key) + + remoteControl.releaseControl() + callEndCallback?.() } const initiateCallEnd = () => { diff --git a/tracker/tracker-assist/src/Mouse.ts b/tracker/tracker-assist/src/Mouse.ts index afb50a1f1..3bef05e62 100644 --- a/tracker/tracker-assist/src/Mouse.ts +++ b/tracker/tracker-assist/src/Mouse.ts @@ -4,16 +4,33 @@ type XY = [number, number] export default class Mouse { private readonly mouse: HTMLDivElement private position: [number,number] = [0,0,] - constructor() { + constructor(private readonly agentName?: string) { this.mouse = document.createElement('div') + const agentBubble = document.createElement('div') + const svg ='' + + this.mouse.innerHTML = svg + + Object.assign(agentBubble.style, { + position: 'absolute', + padding: '4px 6px', + borderRadius: '8px', + backgroundColor: 'green', + color: 'white', + bottom: '-18px', + left: '50%', + fontSize: '16px', + whiteSpace: 'nowrap', + }) + + const agentNameStr = this.agentName ? this.agentName.length > 10 ? this.agentName.slice(0, 9) + '...' : this.agentName : 'Agent' + agentBubble.innerHTML = `${agentNameStr}` + + this.mouse.appendChild(agentBubble) + Object.assign(this.mouse.style, { - width: '20px', - height: '20px', - opacity: '.4', - borderRadius: '50%', position: 'absolute', zIndex: '999998', - background: 'radial-gradient(red, transparent)', }) } @@ -66,7 +83,7 @@ export default class Mouse { let el = this.lastScrEl - // Scroll the same one + // Scroll the same one if (el instanceof Element) { el.scrollLeft += dX el.scrollTop += dY @@ -78,12 +95,12 @@ export default class Mouse { } el = document.elementFromPoint( - mouseX-this.pScrEl.scrollLeft, + mouseX-this.pScrEl.scrollLeft, mouseY-this.pScrEl.scrollTop, ) while (el) { // el.scrollTopMax > 0 // available in firefox - if (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth) { + if (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth) { const styles = getComputedStyle(el) if (styles.overflow.indexOf('scroll') >= 0 || styles.overflow.indexOf('auto') >= 0) { // returns true for body in habr.com but it's not scrollable const esl = el.scrollLeft diff --git a/tracker/tracker-assist/src/RemoteControl.ts b/tracker/tracker-assist/src/RemoteControl.ts index 5a122d837..8c66c8ef3 100644 --- a/tracker/tracker-assist/src/RemoteControl.ts +++ b/tracker/tracker-assist/src/RemoteControl.ts @@ -24,8 +24,8 @@ export default class RemoteControl { constructor( private readonly options: AssistOptions, - private readonly onGrand: (sting?) => void, - private readonly onRelease: (sting?) => void) {} + private readonly onGrand: (string?) => string | undefined, + private readonly onRelease: (string?) => void) {} reconnect(ids: string[]) { const storedID = sessionStorage.getItem(this.options.session_control_peer_key) @@ -39,12 +39,12 @@ export default class RemoteControl { private confirm: ConfirmWindow | null = null requestControl = (id: string) => { if (this.agentID !== null) { - this.releaseControl(id) + this.releaseControl() return } setTimeout(() =>{ if (this.status === RCStatus.Requesting) { - this.releaseControl(id) + this.releaseControl() } }, 30000) this.agentID = id @@ -54,7 +54,7 @@ export default class RemoteControl { if (allowed) { this.grantControl(id) } else { - this.releaseControl(id) + this.releaseControl() } }) .then(() => { @@ -65,23 +65,24 @@ export default class RemoteControl { console.error(e) }) } + grantControl = (id: string) => { this.agentID = id this.status = RCStatus.Enabled - this.mouse = new Mouse() - this.mouse.mount() sessionStorage.setItem(this.options.session_control_peer_key, id) - this.onGrand(id) + const agentName = this.onGrand(id) + this.mouse = new Mouse(agentName) + this.mouse.mount() } - releaseControl = (id: string) => { - if (this.agentID !== id) { return } + releaseControl = () => { + if (!this.agentID) { return } this.mouse?.remove() this.mouse = null this.status = RCStatus.Disabled - this.agentID = null sessionStorage.removeItem(this.options.session_control_peer_key) - this.onRelease(id) + this.onRelease(this.agentID) + this.agentID = null } scroll = (id, d) => { id === this.agentID && this.mouse?.scroll(d) }