feat(assist): 3.5.0: transition to WS, remote control separation + better customize configuration

This commit is contained in:
ShiKhu 2022-02-10 23:51:32 +01:00
parent ff0784bdd9
commit 967b885c16
7 changed files with 119 additions and 93 deletions

View file

@ -1,6 +1,8 @@
node_modules
npm-debug.log
yarn-error.log
lib
cjs
.cache
*.cache
*.DS_Store

View file

@ -1,4 +1,6 @@
src
npm-debug.log
yarn-error.log
tsconfig-cjs.json
tsconfig.json
.prettierrc.json

View file

@ -1,12 +1,14 @@
import type { Socket } from 'socket.io-client';
import io from 'socket.io-client';
import Peer from 'peerjs';
import type { Properties } from 'csstype';
import { App } from '@openreplay/tracker';
import RequestLocalStream from './LocalStream.js';
import Mouse from './Mouse.js';
import CallWindow from './CallWindow.js';
import ConfirmWindow from './ConfirmWindow.js';
import RequestLocalStream from './LocalStream.js';
import ConfirmWindow, { callConfirmDefault, controlConfirmDefault } from './ConfirmWindow.js';
import type { Options as ConfirmOptions } from './ConfirmWindow.js';
//@ts-ignore peerjs hack for webpack5 (?!) TODO: ES/node modules;
@ -15,9 +17,13 @@ Peer = Peer.default || Peer;
export interface Options {
onAgentConnect: () => ((()=>{}) | void),
onCallStart: () => ((()=>{}) | void),
confirmText: string,
confirmStyle: Object, // Styles object
session_calling_peer_key: string,
callConfirm: ConfirmOptions,
controlConfirm: ConfirmOptions,
confirmText?: string, // @depricated
confirmStyle?: Properties, // @depricated
config: RTCConfiguration,
}
@ -44,10 +50,22 @@ export default class Assist {
private callingState: CallingState = CallingState.False
private agents: Record<string, Agent> = {}
private readonly options: Options
constructor(
private readonly app: App,
private readonly options: Options,
private readonly noSecureMode: boolean = false) {
options?: Partial<Options>,
private readonly noSecureMode: boolean = false,
) {
this.options = Object.assign({
session_calling_peer_key: "__openreplay_calling_peer",
config: null,
onCallStart: ()=>{},
onAgentConnect: ()=>{},
callConfirm: {},
controlConfirm: {}, // TODO: clear options passing/merging/overriting
},
options,
);
app.attachStartCallback(() => {
if (this.assistDemandedRestart) { return; }
this.onStart()
@ -123,7 +141,7 @@ export default class Assist {
return
}
controllingAgent = id
confirmRC = new ConfirmWindow("Allow remote control?")
confirmRC = new ConfirmWindow(controlConfirmDefault(this.options.controlConfirm))
confirmRC.mount().then(allowed => {
if (allowed) { // TODO: per agent id
mouse.mount()
@ -206,7 +224,10 @@ export default class Assist {
confirmAnswer = Promise.resolve(true)
} else {
setCallingState(CallingState.Requesting)
confirmCall = new ConfirmWindow(this.options.confirmText, this.options.confirmStyle)
confirmCall = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || {
text: this.options.confirmText,
style: this.options.confirmStyle,
}))
confirmAnswer = confirmCall.mount()
this.onRemoteCallEnd = () => { // if call cancelled by a caller before confirmation
app.debug.log("Received call_end during confirm window opened")

View file

@ -1,49 +0,0 @@
import type { DataConnection } from 'peerjs';
// TODO: proper Message type export from tracker in 3.5.0
interface Message {
encode(w: any): boolean;
}
// 16kb should be max according to specification
// 64kb chrome
const crOrFf: boolean =
typeof navigator !== "undefined" &&
(navigator.userAgent.indexOf("Chrom") !== -1 || // Chrome && Chromium
navigator.userAgent.indexOf("Firefox") !== -1);
const MESSAGES_PER_SEND = crOrFf ? 200 : 50
// Bffering required in case of webRTC
export default class BufferingConnection {
private readonly buffer: Message[][] = []
private buffering: boolean = false
constructor(readonly conn: DataConnection,
private readonly msgsPerSend: number = MESSAGES_PER_SEND){}
private sendNext() {
if (this.buffer.length) {
setTimeout(() => {
this.conn.send(this.buffer.shift())
this.sendNext()
}, 15)
} else {
this.buffering = false
}
}
send(messages: Message[]) {
if (!this.conn.open) { return; }
let i = 0;
//@ts-ignore
messages=messages.filter(m => m._id !== 39)
while (i < messages.length) {
this.buffer.push(messages.slice(i, i+=this.msgsPerSend))
}
if (!this.buffering) {
this.buffering = true
this.sendNext();
}
}
}

View file

@ -1,36 +1,83 @@
import type { Properties } from 'csstype';
const declineIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="22" width="22" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>`;
import { declineCall, acceptCall, cross, remoteControl } from './icons.js'
type ButtonOptions = HTMLButtonElement | string | {
innerHTML: string,
style?: Properties,
}
// TODO: common strategy for InputOptions/defaultOptions merging
interface ConfirmWindowOptions {
text: string,
style?: Properties,
confirmBtn: ButtonOptions,
declineBtn: ButtonOptions,
}
export type Options = string | Partial<ConfirmWindowOptions>
function confirmDefault(
opts: Options,
confirmBtn: ButtonOptions,
declineBtn: ButtonOptions,
text: string,
): ConfirmWindowOptions {
const isStr = typeof opts === "string"
return Object.assign({
text: isStr ? opts : text,
confirmBtn,
declineBtn,
}, isStr ? undefined : opts)
}
export const callConfirmDefault = (opts: Options) =>
confirmDefault(opts, acceptCall, declineCall, "You have an incoming call. Do you want to answer?")
export const controlConfirmDefault = (opts: Options) =>
confirmDefault(opts, remoteControl, cross, "Allow remote control?")
function makeButton(options: ButtonOptions): HTMLButtonElement {
if (options instanceof HTMLButtonElement) {
return options
}
const btn = document.createElement('button')
Object.assign(btn.style, {
background: "transparent",
padding: 0,
margin: 0,
border: 0,
cursor: "pointer",
borderRadius: "50%",
width: "22px",
height: "22px",
color: "white", // TODO: nice text button in case when only text is passed
})
if (typeof options === "string") {
btn.innerHTML = options
} else {
btn.innerHTML = options.innerHTML
Object.assign(btn.style, options.style)
}
return btn
}
export default class ConfirmWindow {
private wrapper: HTMLDivElement;
constructor(text: string, styles?: Object) {
constructor(options: ConfirmWindowOptions) {
const wrapper = document.createElement('div');
const popup = document.createElement('div');
const p = document.createElement('p');
p.innerText = text;
p.innerText = options.text;
const buttons = document.createElement('div');
const answerBtn = document.createElement('button');
answerBtn.innerHTML = declineIcon.replace('fill="#ef5261"', 'fill="green"');
const declineBtn = document.createElement('button');
declineBtn.innerHTML = declineIcon;
buttons.appendChild(answerBtn);
const confirmBtn = makeButton(options.confirmBtn);
const declineBtn = makeButton(options.declineBtn);
buttons.appendChild(confirmBtn);
buttons.appendChild(declineBtn);
popup.appendChild(p);
popup.appendChild(buttons);
const btnStyles = {
borderRadius: "50%",
width: "22px",
height: "22px",
background: "transparent",
padding: 0,
margin: 0,
border: 0,
cursor: "pointer",
}
Object.assign(answerBtn.style, btnStyles);
Object.assign(declineBtn.style, btnStyles);
Object.assign(buttons.style, {
marginTop: "10px",
display: "flex",
@ -51,7 +98,7 @@ export default class ConfirmWindow {
textAlign: "center",
borderRadius: ".25em .25em .4em .4em",
boxShadow: "0 0 20px rgb(0 0 0 / 20%)",
}, styles);
}, options.style);
Object.assign(wrapper.style, {
position: "fixed",
@ -66,7 +113,7 @@ export default class ConfirmWindow {
wrapper.appendChild(popup);
this.wrapper = wrapper;
answerBtn.onclick = () => {
confirmBtn.onclick = () => {
this._remove();
this.resolve(true);
}

View file

@ -0,0 +1,14 @@
// TODO: something with these big strings in bundle?
export const declineCall = `<svg xmlns="http://www.w3.org/2000/svg" height="22" width="22" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>`;
export const acceptCall = declineCall.replace('fill="#ef5261"', 'fill="green"')
export const cross = `<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-x-lg" viewBox="0 0 16 16" fill="#ef5261">
<path fill-rule="evenodd" d="M13.854 2.146a.5.5 0 0 1 0 .708l-11 11a.5.5 0 0 1-.708-.708l11-11a.5.5 0 0 1 .708 0Z"/>
<path fill-rule="evenodd" d="M2.146 2.146a.5.5 0 0 0 0 .708l11 11a.5.5 0 0 0 .708-.708l-11-11a.5.5 0 0 0-.708 0Z"/>
</svg>`
export const remoteControl = `<svg fill="green" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M638.59 368.22l-33.37-211.59c-8.86-50.26-48.4-90.77-100.66-103.13h-.07a803.14 803.14 0 0 0-369 0C83.17 65.86 43.64 106.36 34.78 156.63L1.41 368.22C-8.9 426.73 38.8 480 101.51 480c49.67 0 93.77-30.07 109.48-74.64l7.52-21.36h203l7.49 21.36C444.72 449.93 488.82 480 538.49 480c62.71 0 110.41-53.27 100.1-111.78zm-45.11 54.88c-13.28 15.82-33.33 24.9-55 24.9-36.2 0-68.07-21.41-79.29-53.27l-7.53-21.36-7.52-21.37H195.86l-7.53 21.37-7.53 21.36C169.58 426.59 137.71 448 101.51 448c-21.66 0-41.71-9.08-55-24.9A59.93 59.93 0 0 1 33 373.2l33.28-211c6.66-37.7 36.72-68.14 76.53-77.57a771.07 771.07 0 0 1 354.38 0c39.84 9.42 69.87 39.86 76.42 77l33.47 212.15c3.11 17.64-1.72 35.16-13.6 49.32zm-339.3-218.74h-42.54v-42.54a9.86 9.86 0 0 0-9.82-9.82h-19.64a9.86 9.86 0 0 0-9.82 9.82v42.54h-42.54a9.86 9.86 0 0 0-9.82 9.82v19.64a9.86 9.86 0 0 0 9.82 9.82h42.54v42.54a9.86 9.86 0 0 0 9.82 9.82h19.64a9.86 9.86 0 0 0 9.82-9.82v-42.54h42.54a9.86 9.86 0 0 0 9.82-9.82v-19.64a9.86 9.86 0 0 0-9.82-9.82zM416 224a32 32 0 1 0 32 32 32 32 0 0 0-32-32zm64-64a32 32 0 1 0 32 32 32 32 0 0 0-32-32z"/></svg>`

View file

@ -5,18 +5,7 @@ import type { Options } from './Assist.js'
import Assist from './Assist.js'
export default function(opts?: Partial<Options>) {
const options: Options = Object.assign(
{
confirmText: "You have an incoming call. Do you want to answer?",
confirmStyle: {},
session_calling_peer_key: "__openreplay_calling_peer",
config: null,
onCallStart: ()=>{},
onAgentConnect: ()=>{},
},
opts,
);
export default function(opts?: Partial<Options>) {
return function(app: App | null, appOptions: { __DISABLE_SECURE_MODE?: boolean } = {}) {
// @ts-ignore
if (app === null || !navigator?.mediaDevices?.getUserMedia) { // 93.04% browsers
@ -27,7 +16,7 @@ export default function(opts?: Partial<Options>) {
return
}
app.notify.log("OpenReplay Assist initializing.")
const assist = new Assist(app, options, appOptions.__DISABLE_SECURE_MODE)
const assist = new Assist(app, opts, appOptions.__DISABLE_SECURE_MODE)
app.debug.log(assist)
return assist