Merge remote-tracking branch 'origin/dev' into api-v1.5.5
This commit is contained in:
commit
9e3a5004f0
24 changed files with 923 additions and 245 deletions
|
|
@ -432,7 +432,7 @@ def __get_autocomplete_table(value, project_id):
|
|||
AND value ILIKE %(value)s
|
||||
LIMIT 5)""")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify("UNION ALL".join(sub_queries) + ";",
|
||||
query = cur.mogrify(" UNION ".join(sub_queries) + ";",
|
||||
{"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value)})
|
||||
cur.execute(query)
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ def get_sessions_on_the_fly(funnel_id, project_id, user_id, data: schemas.Funnel
|
|||
data.events = filter_stages(data.events)
|
||||
data.events = __fix_stages(data.events)
|
||||
if len(data.events) == 0:
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id)
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if f is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
get_start_end_time(filter_d=f["filter"], range_value=data.range_value,
|
||||
|
|
@ -217,16 +217,21 @@ def get_top_insights(project_id, user_id, funnel_id, range_value=None, start_dat
|
|||
return {"errors": ["funnel not found"]}
|
||||
get_start_end_time(filter_d=f["filter"], range_value=range_value, start_date=start_date, end_date=end_date)
|
||||
insights, total_drop_due_to_issues = significance.get_top_insights(filter_d=f["filter"], project_id=project_id)
|
||||
insights = helper.list_to_camel_case(insights)
|
||||
if len(insights) > 0:
|
||||
# fix: this fix for huge drop count
|
||||
if total_drop_due_to_issues > insights[0]["sessionsCount"]:
|
||||
total_drop_due_to_issues = insights[0]["sessionsCount"]
|
||||
# end fix
|
||||
insights[-1]["dropDueToIssues"] = total_drop_due_to_issues
|
||||
return {"data": {"stages": helper.list_to_camel_case(insights),
|
||||
return {"data": {"stages": insights,
|
||||
"totalDropDueToIssues": total_drop_due_to_issues}}
|
||||
|
||||
|
||||
def get_top_insights_on_the_fly(funnel_id, user_id, project_id, data: schemas.FunnelInsightsPayloadSchema):
|
||||
data.events = filter_stages(__parse_events(data.events))
|
||||
if len(data.events) == 0:
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id)
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if f is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
get_start_end_time(filter_d=f["filter"], range_value=data.rangeValue,
|
||||
|
|
@ -235,9 +240,14 @@ def get_top_insights_on_the_fly(funnel_id, user_id, project_id, data: schemas.Fu
|
|||
data = schemas.FunnelInsightsPayloadSchema.parse_obj(f["filter"])
|
||||
data.events = __fix_stages(data.events)
|
||||
insights, total_drop_due_to_issues = significance.get_top_insights(filter_d=data.dict(), project_id=project_id)
|
||||
insights = helper.list_to_camel_case(insights)
|
||||
if len(insights) > 0:
|
||||
# fix: this fix for huge drop count
|
||||
if total_drop_due_to_issues > insights[0]["sessionsCount"]:
|
||||
total_drop_due_to_issues = insights[0]["sessionsCount"]
|
||||
# end fix
|
||||
insights[-1]["dropDueToIssues"] = total_drop_due_to_issues
|
||||
return {"data": {"stages": helper.list_to_camel_case(insights),
|
||||
return {"data": {"stages": insights,
|
||||
"totalDropDueToIssues": total_drop_due_to_issues}}
|
||||
|
||||
|
||||
|
|
@ -256,7 +266,7 @@ def get_issues_on_the_fly(funnel_id, user_id, project_id, data: schemas.FunnelSe
|
|||
data.events = filter_stages(data.events)
|
||||
data.events = __fix_stages(data.events)
|
||||
if len(data.events) == 0:
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id)
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if f is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
get_start_end_time(filter_d=f["filter"], range_value=data.rangeValue,
|
||||
|
|
@ -307,7 +317,7 @@ def get(funnel_id, project_id, user_id, flatten=True, fix_stages=True):
|
|||
def search_by_issue(user_id, project_id, funnel_id, issue_id, data: schemas.FunnelSearchPayloadSchema, range_value=None,
|
||||
start_date=None, end_date=None):
|
||||
if len(data.events) == 0:
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id)
|
||||
f = get(funnel_id=funnel_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if f is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
data.startDate = data.startDate if data.startDate is not None else start_date
|
||||
|
|
|
|||
|
|
@ -385,6 +385,19 @@ def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, d
|
|||
return sessions
|
||||
|
||||
|
||||
def __is_valid_event(is_any: bool, event: schemas._SessionSearchEventSchema):
|
||||
return not (not is_any and len(event.value) == 0 and event.type not in [schemas.EventType.request_details,
|
||||
schemas.EventType.graphql_details] \
|
||||
or event.type in [schemas.PerformanceEventType.location_dom_complete,
|
||||
schemas.PerformanceEventType.location_largest_contentful_paint_time,
|
||||
schemas.PerformanceEventType.location_ttfb,
|
||||
schemas.PerformanceEventType.location_avg_cpu_load,
|
||||
schemas.PerformanceEventType.location_avg_memory_usage
|
||||
] and (event.source is None or len(event.source) == 0) \
|
||||
or event.type in [schemas.EventType.request_details, schemas.EventType.graphql_details] and (
|
||||
event.filters is None or len(event.filters) == 0))
|
||||
|
||||
|
||||
def search_query_parts(data, error_status, errors_only, favorite_only, issue, project_id, user_id, extra_event=None):
|
||||
ss_constraints = []
|
||||
full_args = {"project_id": project_id, "startDate": data.startDate, "endDate": data.endDate,
|
||||
|
|
@ -600,6 +613,13 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
value_key=f_k))
|
||||
# ---------------------------------------------------------------------------
|
||||
if len(data.events) > 0:
|
||||
valid_events_count = 0
|
||||
for event in data.events:
|
||||
is_any = _isAny_opreator(event.operator)
|
||||
if not isinstance(event.value, list):
|
||||
event.value = [event.value]
|
||||
if __is_valid_event(is_any=is_any, event=event):
|
||||
valid_events_count += 1
|
||||
events_query_from = []
|
||||
event_index = 0
|
||||
or_events = data.events_order == schemas.SearchEventOrder._or
|
||||
|
|
@ -610,16 +630,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
is_any = _isAny_opreator(event.operator)
|
||||
if not isinstance(event.value, list):
|
||||
event.value = [event.value]
|
||||
if not is_any and len(event.value) == 0 and event_type not in [schemas.EventType.request_details,
|
||||
schemas.EventType.graphql_details] \
|
||||
or event_type in [schemas.PerformanceEventType.location_dom_complete,
|
||||
schemas.PerformanceEventType.location_largest_contentful_paint_time,
|
||||
schemas.PerformanceEventType.location_ttfb,
|
||||
schemas.PerformanceEventType.location_avg_cpu_load,
|
||||
schemas.PerformanceEventType.location_avg_memory_usage
|
||||
] and (event.source is None or len(event.source) == 0) \
|
||||
or event_type in [schemas.EventType.request_details, schemas.EventType.graphql_details] and (
|
||||
event.filters is None or len(event.filters) == 0):
|
||||
if not __is_valid_event(is_any=is_any, event=event):
|
||||
continue
|
||||
op = __get_sql_operator(event.operator)
|
||||
is_not = False
|
||||
|
|
@ -938,7 +949,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
""")
|
||||
else:
|
||||
events_query_from.append(f"""\
|
||||
(SELECT main.session_id, MIN(main.timestamp) AS timestamp
|
||||
(SELECT main.session_id, {"MIN" if event_index < (valid_events_count - 1) else "MAX"}(main.timestamp) AS timestamp
|
||||
FROM {event_from}
|
||||
WHERE {" AND ".join(event_where)}
|
||||
GROUP BY 1
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ def get_issues(stages, rows, first_stage=None, last_stage=None, drop_only=False)
|
|||
split = issue.split('__^__')
|
||||
issues_dict['significant' if is_sign else 'insignificant'].append({
|
||||
"type": split[0],
|
||||
"title": get_issue_title(split[0]),
|
||||
"title": helper.get_issue_title(split[0]),
|
||||
"affected_sessions": affected_sessions[issue],
|
||||
"unaffected_sessions": session_counts[1] - affected_sessions[issue],
|
||||
"lost_conversions": lost_conversions,
|
||||
|
|
@ -641,27 +641,3 @@ def get_overview(filter_d, project_id, first_stage=None, last_stage=None):
|
|||
output['stages'] = stages_list
|
||||
output['criticalIssuesCount'] = n_critical_issues
|
||||
return output
|
||||
|
||||
|
||||
def get_issue_title(issue_type):
|
||||
return {'click_rage': "Click Rage",
|
||||
'dead_click': "Dead Click",
|
||||
'excessive_scrolling': "Excessive Scrolling",
|
||||
'bad_request': "Bad Request",
|
||||
'missing_resource': "Missing Image",
|
||||
'memory': "High Memory Usage",
|
||||
'cpu': "High CPU",
|
||||
'slow_resource': "Slow Resource",
|
||||
'slow_page_load': "Slow Page Performance",
|
||||
'crash': "Crash",
|
||||
'ml_cpu': "High CPU",
|
||||
'ml_memory': "High Memory Usage",
|
||||
'ml_dead_click': "Dead Click",
|
||||
'ml_click_rage': "Click Rage",
|
||||
'ml_mouse_thrashing': "Mouse Thrashing",
|
||||
'ml_excessive_scrolling': "Excessive Scrolling",
|
||||
'ml_slow_resources': "Slow Resource",
|
||||
'custom': "Custom Event",
|
||||
'js_exception': "Error",
|
||||
'custom_event_error': "Custom Error",
|
||||
'js_error': "Error"}.get(issue_type, issue_type)
|
||||
|
|
|
|||
|
|
@ -213,11 +213,11 @@ def values_for_operator(value: Union[str, list], op: schemas.SearchEventOperator
|
|||
if value is None:
|
||||
return value
|
||||
if op == schemas.SearchEventOperator._starts_with:
|
||||
return value + '%'
|
||||
return f"{value}%"
|
||||
elif op == schemas.SearchEventOperator._ends_with:
|
||||
return '%' + value
|
||||
return f"%{value}"
|
||||
elif op == schemas.SearchEventOperator._contains or op == schemas.SearchEventOperator._not_contains:
|
||||
return '%' + value + '%'
|
||||
return f"%{value}%"
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
\set ON_ERROR_STOP true
|
||||
SET client_min_messages TO NOTICE;
|
||||
BEGIN;
|
||||
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||
RETURNS text AS
|
||||
|
|
@ -22,7 +24,7 @@ $$
|
|||
LIMIT 1)
|
||||
ORDER BY LOWER(email)) THEN
|
||||
raise notice 'duplicate users detected';
|
||||
FOR duplicate IN SELECT user_id, email, deleted_at, verified_email, jwt_iat
|
||||
FOR duplicate IN SELECT user_id, email, deleted_at, jwt_iat
|
||||
FROM users
|
||||
WHERE lower(email) =
|
||||
(SELECT LOWER(email)
|
||||
|
|
|
|||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
|
|
@ -8,3 +8,4 @@ app/components/ui/SVG.js
|
|||
*.DS_Store
|
||||
.env
|
||||
*css.d.ts
|
||||
*.cache
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ const DOCUMENTATION = 'NPM';
|
|||
// const SEGMENT = 'SEGMENT';
|
||||
// const GOOGLE_TAG = 'GOOGLE TAG';
|
||||
const TABS = [
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
// { key: SEGMENT, text: SEGMENT },
|
||||
// { key: GOOGLE_TAG, text: GOOGLE_TAG }
|
||||
];
|
||||
|
||||
class TrackingCodeModal extends React.PureComponent {
|
||||
state = { copied: false, changed: false, activeTab: PROJECT };
|
||||
state = { copied: false, changed: false, activeTab: DOCUMENTATION };
|
||||
|
||||
setActiveTab = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import cn from 'classnames';
|
|||
const PROJECT = 'Using Script';
|
||||
const DOCUMENTATION = 'Using NPM';
|
||||
const TABS = [
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION }
|
||||
];
|
||||
|
||||
class TrackingCodeModal extends React.PureComponent {
|
||||
state = { copied: false, changed: false, activeTab: PROJECT };
|
||||
state = { copied: false, changed: false, activeTab: DOCUMENTATION };
|
||||
|
||||
setActiveTab = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@ export default abstract class BaseScreen {
|
|||
|
||||
const screen = document.createElement('div');
|
||||
|
||||
setTimeout(function() {
|
||||
iframe.contentDocument?.addEventListener('mousemove', function() {
|
||||
overlay.style.display = 'block';
|
||||
})
|
||||
|
||||
overlay.addEventListener('contextmenu', function() {
|
||||
overlay.style.display = 'none';
|
||||
})
|
||||
}, 10)
|
||||
|
||||
screen.className = styles.screen;
|
||||
screen.appendChild(iframe);
|
||||
screen.appendChild(overlay);
|
||||
|
|
@ -58,6 +48,20 @@ export default abstract class BaseScreen {
|
|||
// parentElement.onresize = this.scale;
|
||||
window.addEventListener('resize', this.scale);
|
||||
this.scale();
|
||||
|
||||
/* == For the Inspecting Document content == */
|
||||
this.overlay.addEventListener('contextmenu', () => {
|
||||
this.overlay.style.display = 'none'
|
||||
const doc = this.document
|
||||
if (!doc) { return }
|
||||
const returnOverlay = () => {
|
||||
this.overlay.style.display = 'block'
|
||||
doc.removeEventListener('mousemove', returnOverlay)
|
||||
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
|
||||
}
|
||||
doc.addEventListener('mousemove', returnOverlay)
|
||||
doc.addEventListener('mouseclick', returnOverlay)
|
||||
})
|
||||
}
|
||||
|
||||
get window(): WindowProxy | null {
|
||||
|
|
@ -70,10 +74,10 @@ export default abstract class BaseScreen {
|
|||
|
||||
private boundingRect: DOMRect | null = null;
|
||||
private getBoundingClientRect(): DOMRect {
|
||||
//if (this.boundingRect === null) {
|
||||
return this.boundingRect = this.overlay.getBoundingClientRect(); // expensive operation?
|
||||
//}
|
||||
//return this.boundingRect;
|
||||
if (this.boundingRect === null) {
|
||||
return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation?
|
||||
}
|
||||
return this.boundingRect
|
||||
}
|
||||
|
||||
getInternalViewportCoordinates({ x, y }: Point): Point {
|
||||
|
|
@ -85,17 +89,22 @@ export default abstract class BaseScreen {
|
|||
const screenX = (x - overlayX) * scale;
|
||||
const screenY = (y - overlayY) * scale;
|
||||
|
||||
return { x: screenX, y: screenY };
|
||||
return { x: Math.round(screenX), y: Math.round(screenY) };
|
||||
}
|
||||
|
||||
getCurrentScroll(): Point {
|
||||
const docEl = this.document?.documentElement
|
||||
const x = docEl ? docEl.scrollLeft : 0
|
||||
const y = docEl ? docEl.scrollTop : 0
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
getInternalCoordinates(p: Point): Point {
|
||||
const { x, y } = this.getInternalViewportCoordinates(p);
|
||||
|
||||
const docEl = this.document?.documentElement
|
||||
const scrollX = docEl ? docEl.scrollLeft : 0
|
||||
const scrollY = docEl ? docEl.scrollTop : 0
|
||||
const sc = this.getCurrentScroll()
|
||||
|
||||
return { x: x+scrollX, y: y+scrollY };
|
||||
return { x: x+sc.x, y: y+sc.y };
|
||||
}
|
||||
|
||||
getElementFromInternalPoint({ x, y }: Point): Element | null {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.screen {
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transform-origin: left top;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
export default class AnnotationCanvas {
|
||||
readonly canvas: HTMLCanvasElement
|
||||
private ctx: CanvasRenderingContext2D | null = null
|
||||
private painting: boolean = false
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas')
|
||||
Object.assign(this.canvas.style, {
|
||||
position: "fixed",
|
||||
cursor: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAAXNSR0IArs4c6QAAAWNJREFUOE+l1D1Lw1AUBuD35Catg5NzaCMRMilINnGok7sguLg4OlRcBTd/hqBVB0ed7KDgIPgXhJoaG10Kgk4a83EkhcYYktimd703z31zzuESSqwGIDs1bRvAIiRcWrZ9ETFUwhJ6XTsDsPH7Le1bz08H42JkGMa09+W2CVhKBmHC7jhYlOgUTPdUEa3Q86+SIDN/j4olf43BtJMFjoJl1AgMUJMUcRInZHT+w7KgYakGoDxVafmue0hBsJeLmaapvPffziFhraDjDMKWZdvHRaNRlCi2mUNHYl55dBwrDysFZWGloTQ2EZTEJoZiTFXVmaos34Ixn9e5qNgCaHR6vW7emcFozNVmN1ERbfb9myww3bVCTK9rPsDrpCh37HnXAC3Ek5lqf9ErM0im1zUG8BmGtCqq4mEIjppoeEESA5g/JIkaLMuv7AVHEgfNohqlU/7Fol3mPodiufvS7Yz7cP4ARjbPWyYPZSMAAAAASUVORK5CYII=') 0 20, crosshair",
|
||||
left: 0,
|
||||
top: 0,
|
||||
//zIndex: 2147483647 - 2,
|
||||
})
|
||||
}
|
||||
|
||||
isPainting() {
|
||||
return this.painting
|
||||
}
|
||||
|
||||
private resizeCanvas = () => {
|
||||
if (!this.canvas.parentElement) { return }
|
||||
this.canvas.width = this.canvas.parentElement.offsetWidth
|
||||
this.canvas.height = this.canvas.parentElement.offsetHeight
|
||||
}
|
||||
|
||||
private lastPosition: [number, number] = [0,0]
|
||||
start = (p: [number, number]) => {
|
||||
this.painting = true
|
||||
this.clrTmID && clearTimeout(this.clrTmID)
|
||||
this.lastPosition = p
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this.painting = false
|
||||
this.fadeOut()
|
||||
}
|
||||
|
||||
move = (p: [number, number]) =>{
|
||||
if (!this.ctx || !this.painting) { return }
|
||||
this.ctx.globalAlpha = 1.0
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(this.lastPosition[0], this.lastPosition[1])
|
||||
this.ctx.lineTo(p[0], p[1])
|
||||
this.ctx.lineWidth = 8
|
||||
this.ctx.lineCap = "round"
|
||||
this.ctx.lineJoin = "round"
|
||||
this.ctx.strokeStyle = "red"
|
||||
this.ctx.stroke()
|
||||
this.lastPosition = p
|
||||
}
|
||||
|
||||
clrTmID: ReturnType<typeof setTimeout> | null = null
|
||||
private fadeOut() {
|
||||
let timeoutID: ReturnType<typeof setTimeout>
|
||||
const fadeStep = () => {
|
||||
if (!this.ctx || this.painting ) { return }
|
||||
this.ctx.globalCompositeOperation = 'destination-out'
|
||||
this.ctx.fillStyle = "rgba(255, 255, 255, 0.1)"
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.globalCompositeOperation = 'source-over'
|
||||
timeoutID = setTimeout(fadeStep,100)
|
||||
}
|
||||
this.clrTmID = setTimeout(() => {
|
||||
clearTimeout(timeoutID)
|
||||
this.ctx &&
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}, 3700)
|
||||
fadeStep()
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement) {
|
||||
parent.appendChild(this.canvas)
|
||||
this.ctx = this.canvas.getContext("2d")
|
||||
window.addEventListener("resize", this.resizeCanvas)
|
||||
this.resizeCanvas()
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.canvas.parentNode){
|
||||
this.canvas.parentNode.removeChild(this.canvas)
|
||||
}
|
||||
window.removeEventListener("resize", this.resizeCanvas)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import store from 'App/store';
|
|||
import type { LocalStream } from './LocalStream';
|
||||
import { update, getState } from '../../store';
|
||||
import { iceServerConfigFromString } from 'App/utils'
|
||||
|
||||
import MStreamReader from '../messages/MStreamReader';;
|
||||
import AnnotationCanvas from './AnnotationCanvas';
|
||||
import MStreamReader from '../messages/MStreamReader';
|
||||
import JSONRawMessageReader from '../messages/JSONRawMessageReader'
|
||||
|
||||
export enum CallingState {
|
||||
|
|
@ -136,12 +136,13 @@ export default class AssistManager {
|
|||
//socket.onAny((...args) => console.log(...args))
|
||||
socket.on("connect", () => {
|
||||
waitingForMessages = true
|
||||
this.setStatus(ConnectionStatus.WaitingMessages)
|
||||
this.setStatus(ConnectionStatus.WaitingMessages) // TODO: happens frequently on bad network
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
this.toggleRemoteControl(false)
|
||||
})
|
||||
socket.on('messages', messages => {
|
||||
//console.log(messages.filter(m => m._id === 41 || m._id === 44))
|
||||
showDisconnectTimeout && clearTimeout(showDisconnectTimeout);
|
||||
jmr.append(messages) // as RawMessage[]
|
||||
|
||||
|
|
@ -173,9 +174,8 @@ export default class AssistManager {
|
|||
this.setStatus(ConnectionStatus.Disconnected)
|
||||
}, 30000)
|
||||
|
||||
if (getState().remoteControl === RemoteControlStatus.Requesting ||
|
||||
getState().remoteControl === RemoteControlStatus.Enabled) {
|
||||
this.toggleRemoteControl(false)
|
||||
if (getState().remoteControl === RemoteControlStatus.Requesting) {
|
||||
this.toggleRemoteControl(false) // else its remaining
|
||||
}
|
||||
|
||||
// Call State
|
||||
|
|
@ -200,7 +200,7 @@ export default class AssistManager {
|
|||
private onMouseMove = (e: MouseEvent): void => {
|
||||
if (!this.socket) { return }
|
||||
const data = this.md.getInternalCoordinates(e)
|
||||
this.socket.emit("move", [ Math.round(data.x), Math.round(data.y) ])
|
||||
this.socket.emit("move", [ data.x, data.y ])
|
||||
}
|
||||
|
||||
private onWheel = (e: WheelEvent): void => {
|
||||
|
|
@ -213,15 +213,23 @@ export default class AssistManager {
|
|||
|
||||
private onMouseClick = (e: MouseEvent): void => {
|
||||
if (!this.socket) { return; }
|
||||
const data = this.md.getInternalViewportCoordinates(e);
|
||||
const data = this.md.getInternalViewportCoordinates(e)
|
||||
// const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager
|
||||
const el = this.md.getElementFromInternalPoint(data)
|
||||
if (el instanceof HTMLElement) {
|
||||
el.focus()
|
||||
el.oninput = e => e.preventDefault();
|
||||
el.onkeydown = e => e.preventDefault();
|
||||
el.oninput = e => {
|
||||
if (el instanceof HTMLTextAreaElement
|
||||
|| el instanceof HTMLInputElement
|
||||
) {
|
||||
this.socket && this.socket.emit("input", el.value)
|
||||
} else if (el.isContentEditable) {
|
||||
this.socket && this.socket.emit("input", el.innerText)
|
||||
}
|
||||
}
|
||||
//el.onkeydown = e => e.preventDefault()
|
||||
}
|
||||
this.socket.emit("click", [ Math.round(data.x), Math.round(data.y) ]);
|
||||
this.socket.emit("click", [ data.x, data.y ]);
|
||||
}
|
||||
|
||||
private toggleRemoteControl(newState: boolean){
|
||||
|
|
@ -310,6 +318,8 @@ export default class AssistManager {
|
|||
this.callConnection && this.callConnection.close()
|
||||
update({ calling: CallingState.NoCall })
|
||||
this.callArgs = null
|
||||
this.annot?.remove()
|
||||
this.annot = null
|
||||
}
|
||||
|
||||
private initiateCallEnd = () => {
|
||||
|
|
@ -355,6 +365,8 @@ export default class AssistManager {
|
|||
}
|
||||
}
|
||||
|
||||
private annot: AnnotationCanvas | null = null
|
||||
|
||||
private _call() {
|
||||
if (![CallingState.NoCall, CallingState.Reconnecting].includes(getState().calling)) { return }
|
||||
update({ calling: CallingState.Connecting })
|
||||
|
|
@ -379,6 +391,34 @@ export default class AssistManager {
|
|||
call.on('stream', stream => {
|
||||
update({ calling: CallingState.OnCall })
|
||||
this.callArgs && this.callArgs.onStream(stream)
|
||||
|
||||
if (!this.annot) {
|
||||
const annot = this.annot = new AnnotationCanvas()
|
||||
annot.mount(this.md.overlay)
|
||||
annot.canvas.addEventListener("mousedown", e => {
|
||||
if (!this.socket) { return }
|
||||
const data = this.md.getInternalViewportCoordinates(e)
|
||||
annot.start([ data.x, data.y ])
|
||||
this.socket.emit("startAnnotation", [ data.x, data.y ])
|
||||
})
|
||||
annot.canvas.addEventListener("mouseleave", () => {
|
||||
if (!this.socket) { return }
|
||||
annot.stop()
|
||||
this.socket.emit("stopAnnotation")
|
||||
})
|
||||
annot.canvas.addEventListener("mouseup", () => {
|
||||
if (!this.socket) { return }
|
||||
annot.stop()
|
||||
this.socket.emit("stopAnnotation")
|
||||
})
|
||||
annot.canvas.addEventListener("mousemove", e => {
|
||||
if (!this.socket || !annot.isPainting()) { return }
|
||||
|
||||
const data = this.md.getInternalViewportCoordinates(e)
|
||||
annot.move([ data.x, data.y ])
|
||||
this.socket.emit("moveAnnotation", [ data.x, data.y ])
|
||||
})
|
||||
}
|
||||
});
|
||||
//call.peerConnection.addEventListener("track", e => console.log('newtrack',e.track))
|
||||
|
||||
|
|
@ -409,6 +449,10 @@ export default class AssistManager {
|
|||
this.socket.close()
|
||||
document.removeEventListener('visibilitychange', this.onVisChange)
|
||||
}
|
||||
if (this.annot) {
|
||||
this.annot.remove()
|
||||
this.annot = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const oss = {
|
|||
ORIGIN: () => 'window.location.origin',
|
||||
API_EDP: () => 'window.location.origin + "/api"',
|
||||
ASSETS_HOST: () => 'window.location.origin + "/assets"',
|
||||
VERSION: '1.5.3',
|
||||
VERSION: '1.5.4',
|
||||
SOURCEMAP: true,
|
||||
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||
MINIO_PORT: process.env.MINIO_PORT,
|
||||
|
|
@ -21,7 +21,7 @@ const oss = {
|
|||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
ICE_SERVERS: process.env.ICE_SERVERS,
|
||||
TRACKER_VERSION: '3.5.3' // trackerInfo.version,
|
||||
TRACKER_VERSION: '3.5.4' // trackerInfo.version,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
\set ON_ERROR_STOP true
|
||||
SET client_min_messages TO NOTICE;
|
||||
BEGIN;
|
||||
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||
RETURNS text AS
|
||||
|
|
@ -22,7 +24,7 @@ $$
|
|||
LIMIT 1)
|
||||
ORDER BY LOWER(email)) THEN
|
||||
raise notice 'duplicate users detected';
|
||||
FOR duplicate IN SELECT user_id, email, deleted_at, verified_email, jwt_iat
|
||||
FOR duplicate IN SELECT user_id, email, deleted_at, jwt_iat
|
||||
FROM users
|
||||
WHERE lower(email) =
|
||||
(SELECT LOWER(email)
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ function migrate() {
|
|||
IFS=',' read -r -a migration_versions <<< "$1"
|
||||
for version in ${migration_versions[*]}; do
|
||||
echo "Migrating postgresql version $version"
|
||||
psql -f ${pgdir}/${version}/${version}.sql
|
||||
psql -f ${pgdir}/${version}/${version}.sql 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
function init() {
|
||||
echo "Initializing postgresql"
|
||||
psql -f ${pgdir}/init_schema.sql
|
||||
psql -f ${pgdir}/init_schema.sql 2>&1
|
||||
}
|
||||
|
||||
# /bin/bash postgresql.sh migrate $migration_versions
|
||||
|
|
|
|||
|
|
@ -9,8 +9,15 @@
|
|||
<!--CSS -->
|
||||
<!-- <link href="css/styles.css" rel="stylesheet"> -->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.connecting-message {
|
||||
margin-top: 50%;
|
||||
/* margin-top: 50%; */
|
||||
font-size: 20px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
|
|
@ -19,31 +26,90 @@
|
|||
}
|
||||
|
||||
.status-connecting .connecting-message {
|
||||
display: block;
|
||||
/* display: block; */
|
||||
}
|
||||
.status-connecting .card {
|
||||
display: none;
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
.card{
|
||||
min-width: 324px;
|
||||
width: 350px;
|
||||
max-width: 800px;
|
||||
/*min-height: 220px;*/
|
||||
max-height: 450px;
|
||||
/*resize: both;
|
||||
overflow: auto;*/
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
/* min-width: 324px; */
|
||||
width: 300px;
|
||||
/* max-width: 800px; */
|
||||
/* border: solid thin #ccc; */
|
||||
/* box-shadow: 0 0 10px #aaa; */
|
||||
border: solid 4px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.card-footers {
|
||||
display: flex;
|
||||
border-bottom: solid thin #CCC;
|
||||
padding: 5px 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-footers .assist-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #CC0000 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #FF0000 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 5px 8px;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.card .card-header{
|
||||
cursor: move;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: solid thin #ccc;
|
||||
}
|
||||
|
||||
#agent-name, #duration{
|
||||
cursor:default;
|
||||
}
|
||||
|
||||
#video-container {
|
||||
background-color: rgb(90, 90, 90);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* width: 300px; */
|
||||
}
|
||||
|
||||
#video-container video {
|
||||
width: 100% !important;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
#local-stream, #remote-stream {
|
||||
display:none;
|
||||
/* display:none; */ /* TODO uncomment this line */
|
||||
}
|
||||
#video-container.remote #remote-stream {
|
||||
display: block;
|
||||
|
|
@ -57,20 +123,30 @@
|
|||
|
||||
#local-stream{
|
||||
width: 35%;
|
||||
/* top: 50%; */
|
||||
/* left: 70%; */
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
border: thin solid rgba(255,255,255, .3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
#audio-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#audio-btn .bi-mic {
|
||||
fill: #CC0000;
|
||||
}
|
||||
#audio-btn .bi-mic-mute {
|
||||
display:none;
|
||||
}
|
||||
#audio-btn:after {
|
||||
text-transform: capitalize;
|
||||
content: 'Mute'
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Mute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#audio-btn.muted .bi-mic-mute {
|
||||
display: inline-block;
|
||||
|
|
@ -79,19 +155,26 @@
|
|||
display:none;
|
||||
}
|
||||
#audio-btn.muted:after {
|
||||
content: 'Unmute'
|
||||
content: 'Unmute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
#video-btn .bi-camera-video {
|
||||
fill: #CC0000;
|
||||
}
|
||||
#video-btn .bi-camera-video-off {
|
||||
display:none;
|
||||
}
|
||||
#video-btn:after {
|
||||
text-transform: capitalize;
|
||||
content: 'Stop Video'
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Stop Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#video-btn.off:after {
|
||||
content: 'Start Video'
|
||||
content: 'Start Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#video-btn.off .bi-camera-video-off {
|
||||
display: inline-block;
|
||||
|
|
@ -100,16 +183,201 @@
|
|||
display:none;
|
||||
}
|
||||
|
||||
/* CHART */
|
||||
#chat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages { display: none; }
|
||||
#chat-card .chat-input { display: none; }
|
||||
#chat-card .chat-header .arrow-state { transform: rotate(180deg); }
|
||||
#chat-card.active .chat-messages { display: flex; }
|
||||
#chat-card.active .chat-input { display: flex; }
|
||||
#chat-card.active .chat-header .arrow-state { transform: rotate(0deg); }
|
||||
|
||||
#chat-card .chat-header {
|
||||
border-bottom: solid thin #ccc;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chat-card .chat-header .chat-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#chat-card .chat-header .chat-title span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages {
|
||||
padding: 8px 16px;
|
||||
overflow-y: auto;
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#chat-card .message-text {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
color: #666666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#chat-card .message .message-text {
|
||||
/* max-width: 70%; */
|
||||
width: fit-content;
|
||||
}
|
||||
#chat-card .message {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.left .message-text {
|
||||
text-align: left;
|
||||
background: #D7E2E2;
|
||||
border-radius: 0px 30px 30px 30px;
|
||||
}
|
||||
|
||||
#chat-card .message .message-user {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999999;
|
||||
}
|
||||
#chat-card .message .message-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right .message-text {
|
||||
background: #E4E4E4;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 30px 30px 0px 30px;
|
||||
}
|
||||
|
||||
#chat-card .chat-input {
|
||||
margin: 10px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
background-color: #DDDDDD;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chat-card .chat-input .input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background-color: #AAA;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
.send-btn:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
.send-btn svg {
|
||||
fill: #DDDDDD;
|
||||
}
|
||||
|
||||
.confirm-window .title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.confirm-window {
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #F3F3F3;
|
||||
border-radius: 3px;
|
||||
/* position: absolute; */
|
||||
width: fit-content;
|
||||
color: #666666;
|
||||
display: none;
|
||||
}
|
||||
.confirm-window .actions {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
box-shadow: 0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: rgba(0, 167, 47, 1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* .btn-error:hover,
|
||||
.btn-success:hover {
|
||||
filter: brightness(0.9);
|
||||
} */
|
||||
|
||||
.btn-error {
|
||||
background: #FFE9E9;
|
||||
/* border-color: #d43f3a; */
|
||||
color: #CC0000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="remote-control-confirm" class="confirm-window">
|
||||
<div class="title">The agent is requesting remote control</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">Grant remote access</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="call-confirm" class="confirm-window">
|
||||
<div class="title">Answer the call so the agent can assist.</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-telephone" viewBox="0 0 16 16">
|
||||
<path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
|
||||
</svg>
|
||||
<span>Answer</span>
|
||||
</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
<section id="or-assist" class="status-connecting">
|
||||
<div class="connecting-message"> Connecting... </div>
|
||||
<div class="card border-dark shadow">
|
||||
<div class="card shadow">
|
||||
<div class="drag-area card-header d-flex justify-content-between">
|
||||
<div class="user-info">
|
||||
<span>Call with</span>
|
||||
|
|
@ -123,44 +391,98 @@
|
|||
</div>
|
||||
<div id="video-container" class="card-body bg-dark p-0 d-flex align-items-center position-relative">
|
||||
<div id="local-stream" class="ratio ratio-4x3 rounded m-0 p-0 shadow">
|
||||
<p class="text-white m-auto text-center">Starting video...</p>
|
||||
<!-- <p class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-local" autoplay muted></video>
|
||||
</div>
|
||||
|
||||
<div id="remote-stream" class="ratio ratio-4x3 m-0 p-0">
|
||||
<p id="remote-stream-placeholder" class="text-white m-auto text-center">Starting video...</p>
|
||||
<!-- <p id="remote-stream-placeholder" class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-remote" autoplay></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between">
|
||||
<div class="card-footers">
|
||||
<div class="assist-controls">
|
||||
<a href="#" id="audio-btn" class="btn btn-light btn-sm text-uppercase me-2"><i>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-mic" viewBox="0 0 16 16">
|
||||
<!-- Add class .muted to #audio-btn when user mutes audio -->
|
||||
<button
|
||||
href="#"
|
||||
id="audio-btn"
|
||||
class="btn btn-light btn-sm text-uppercase me-2"
|
||||
>
|
||||
<i>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-mic" viewBox="0 0 16 16">
|
||||
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-mic-mute" viewBox="0 0 16 16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-mic-mute" viewBox="0 0 16 16">
|
||||
<path d="M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1zm-5 4c.818 0 1.578-.245 2.212-.667l.718.719a4.973 4.973 0 0 1-2.43.923V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 1 0v1a4 4 0 0 0 4 4zm3-9v4.879l-1-1V3a2 2 0 0 0-3.997-.118l-.845-.845A3.001 3.001 0 0 1 11 3z"/>
|
||||
<path d="m9.486 10.607-.748-.748A2 2 0 0 1 6 8v-.878l-1-1V8a3 3 0 0 0 4.486 2.607zm-7.84-9.253 12 12 .708-.708-12-12-.708.708z"/>
|
||||
</svg>
|
||||
|
||||
</i></a>
|
||||
<!-- Add class .mute to #audio-btn when user mutes audio -->
|
||||
<a href="#" id="video-btn" class="off btn btn-light btn-sm text-uppercase ms-2"><i >
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<!--Add class .off to #video-btn when user stops video -->
|
||||
<button
|
||||
href="#"
|
||||
id="video-btn"
|
||||
class="btn btn-light btn-sm text-uppercase ms-2"
|
||||
>
|
||||
<i>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-camera-video" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-camera-video-off" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10.961 12.365a1.99 1.99 0 0 0 .522-1.103l3.11 1.382A1 1 0 0 0 16 11.731V4.269a1 1 0 0 0-1.406-.913l-3.111 1.382A2 2 0 0 0 9.5 3H4.272l.714 1H9.5a1 1 0 0 1 1 1v6a1 1 0 0 1-.144.518l.605.847zM1.428 4.18A.999.999 0 0 0 1 5v6a1 1 0 0 0 1 1h5.014l.714 1H2a2 2 0 0 1-2-2V5c0-.675.334-1.272.847-1.634l.58.814zM15 11.73l-3.5-1.555v-4.35L15 4.269v7.462zm-4.407 3.56-10-14 .814-.58 10 14-.814.58z"/>
|
||||
</svg>
|
||||
</i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id="end-call-btn" href="#" class="btn btn-danger btn-sm text-uppercase" style="margin-right: 8px;">End</button>
|
||||
</div>
|
||||
|
||||
</i></a>
|
||||
<!--Add class .off to #video-btn when user stops video -->
|
||||
<!-- CHAT - add .active class to show the messages and input -->
|
||||
<div id="chat-card" class="active">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16">
|
||||
<path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/>
|
||||
</svg>
|
||||
<span>Chat</span>
|
||||
</div>
|
||||
<div class="assist-end">
|
||||
<a id="end-call-btn" href="#" class="btn btn-danger btn-sm text-uppercase">End</a>
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" class="bi bi-chevron-up arrow-state" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<div class="message left">
|
||||
<div class="message-text"> Hey, did you get the key? </div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time"> 00:00 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message right">
|
||||
<div class="message-text">
|
||||
Oui, merci!
|
||||
</div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time">00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<input type="text" class="input" placeholder="Type a message...">
|
||||
<div class="send-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-assist",
|
||||
"description": "Tracker plugin for screen assistance through the WebRTC",
|
||||
"version": "3.5.5",
|
||||
"version": "3.5.6",
|
||||
"keywords": [
|
||||
"WebRTC",
|
||||
"assistance",
|
||||
|
|
|
|||
80
tracker/tracker-assist/src/AnnotationCanvas.ts
Normal file
80
tracker/tracker-assist/src/AnnotationCanvas.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
export default class AnnotationCanvas {
|
||||
private canvas: HTMLCanvasElement
|
||||
private ctx: CanvasRenderingContext2D | null = null
|
||||
private painting: boolean = false
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas')
|
||||
Object.assign(this.canvas.style, {
|
||||
position: "fixed",
|
||||
left: 0,
|
||||
top: 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: 2147483647 - 2,
|
||||
})
|
||||
}
|
||||
|
||||
private resizeCanvas = () => {
|
||||
this.canvas.width = window.innerWidth
|
||||
this.canvas.height = window.innerHeight
|
||||
}
|
||||
|
||||
private lastPosition: [number, number] = [0,0]
|
||||
start = (p: [number, number]) => {
|
||||
this.painting = true
|
||||
this.clrTmID && clearTimeout(this.clrTmID)
|
||||
this.lastPosition = p
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this.painting = false
|
||||
this.fadeOut()
|
||||
}
|
||||
|
||||
move = (p: [number, number]) =>{
|
||||
if (!this.ctx || !this.painting) { return }
|
||||
this.ctx.globalAlpha = 1.0
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(this.lastPosition[0], this.lastPosition[1])
|
||||
this.ctx.lineTo(p[0], p[1])
|
||||
this.ctx.lineWidth = 8
|
||||
this.ctx.lineCap = "round"
|
||||
this.ctx.lineJoin = "round"
|
||||
this.ctx.strokeStyle = "red"
|
||||
this.ctx.stroke()
|
||||
this.lastPosition = p
|
||||
}
|
||||
|
||||
clrTmID: ReturnType<typeof setTimeout> | null = null
|
||||
private fadeOut() {
|
||||
let timeoutID: ReturnType<typeof setTimeout>
|
||||
const fadeStep = () => {
|
||||
if (!this.ctx || this.painting ) { return }
|
||||
this.ctx.globalCompositeOperation = 'destination-out'
|
||||
this.ctx.fillStyle = "rgba(255, 255, 255, 0.1)"
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.globalCompositeOperation = 'source-over'
|
||||
timeoutID = setTimeout(fadeStep,100)
|
||||
}
|
||||
this.clrTmID = setTimeout(() => {
|
||||
clearTimeout(timeoutID)
|
||||
this.ctx &&
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}, 4000)
|
||||
fadeStep()
|
||||
}
|
||||
|
||||
|
||||
mount() {
|
||||
document.body.appendChild(this.canvas)
|
||||
this.ctx = this.canvas.getContext("2d")
|
||||
window.addEventListener("resize", this.resizeCanvas)
|
||||
this.resizeCanvas()
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.canvas.parentNode){
|
||||
this.canvas.parentNode.removeChild(this.canvas)
|
||||
}
|
||||
window.removeEventListener("resize", this.resizeCanvas)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,9 @@ import type { Properties } from 'csstype';
|
|||
import { App } from '@openreplay/tracker';
|
||||
|
||||
import RequestLocalStream from './LocalStream.js';
|
||||
import Mouse from './Mouse.js';
|
||||
import RemoteControl from './RemoteControl.js';
|
||||
import CallWindow from './CallWindow.js';
|
||||
import AnnotationCanvas from './AnnotationCanvas.js';
|
||||
import ConfirmWindow, { callConfirmDefault, controlConfirmDefault } from './ConfirmWindow.js';
|
||||
import type { Options as ConfirmOptions } from './ConfirmWindow.js';
|
||||
|
||||
|
|
@ -14,12 +15,12 @@ import type { Options as ConfirmOptions } from './ConfirmWindow.js';
|
|||
//@ts-ignore peerjs hack for webpack5 (?!) TODO: ES/node modules;
|
||||
Peer = Peer.default || Peer;
|
||||
|
||||
type BehinEndCallback = () => ((()=>{}) | void)
|
||||
type StartEndCallback = () => ((()=>{}) | void)
|
||||
|
||||
export interface Options {
|
||||
onAgentConnect: BehinEndCallback,
|
||||
onCallStart: BehinEndCallback,
|
||||
onRemoteControlStart: BehinEndCallback,
|
||||
onAgentConnect: StartEndCallback,
|
||||
onCallStart: StartEndCallback,
|
||||
onRemoteControlStart: StartEndCallback,
|
||||
session_calling_peer_key: string,
|
||||
session_control_peer_key: string,
|
||||
callConfirm: ConfirmOptions,
|
||||
|
|
@ -39,8 +40,11 @@ enum CallingState {
|
|||
};
|
||||
|
||||
|
||||
// TODO typing????
|
||||
type OptionalCallback = (()=>{}) | void
|
||||
type Agent = {
|
||||
onDisconnect: ((()=>{}) | void), // TODO: better types here
|
||||
onDisconnect?: OptionalCallback,
|
||||
onControlReleased?: OptionalCallback,
|
||||
name?: string
|
||||
//
|
||||
}
|
||||
|
|
@ -139,6 +143,34 @@ export default class Assist {
|
|||
})
|
||||
socket.onAny((...args) => app.debug.log("Socket:", ...args))
|
||||
|
||||
|
||||
const remoteControl = new RemoteControl(
|
||||
this.options,
|
||||
id => {
|
||||
this.agents[id].onControlReleased = this.options.onRemoteControlStart()
|
||||
this.emit("control_granted", id)
|
||||
},
|
||||
id => {
|
||||
const cb = this.agents[id].onControlReleased
|
||||
delete this.agents[id].onControlReleased
|
||||
typeof cb === "function" && cb()
|
||||
this.emit("control_rejected", id)
|
||||
},
|
||||
)
|
||||
|
||||
// TODO: check incoming args
|
||||
socket.on("request_control", remoteControl.requestControl)
|
||||
socket.on("release_control", remoteControl.releaseControl)
|
||||
socket.on("scroll", remoteControl.scroll)
|
||||
socket.on("click", remoteControl.click)
|
||||
socket.on("move", remoteControl.move)
|
||||
socket.on("input", remoteControl.input)
|
||||
|
||||
let annot: AnnotationCanvas | null = null
|
||||
socket.on("moveAnnotation", (_, p) => annot && annot.move(p)) // TODO: restrict by id
|
||||
socket.on("startAnnotation", (_, p) => annot && annot.start(p))
|
||||
socket.on("stopAnnotation", () => annot && annot.stop())
|
||||
|
||||
socket.on("NEW_AGENT", (id: string, info) => {
|
||||
this.agents[id] = {
|
||||
onDisconnect: this.options.onAgentConnect && this.options.onAgentConnect(),
|
||||
|
|
@ -148,7 +180,7 @@ export default class Assist {
|
|||
this.app.stop();
|
||||
this.app.start().then(() => { this.assistDemandedRestart = false })
|
||||
})
|
||||
socket.on("AGENTS_CONNECTED", (ids) => {
|
||||
socket.on("AGENTS_CONNECTED", (ids: string[]) => {
|
||||
ids.forEach(id =>{
|
||||
this.agents[id] = {
|
||||
onDisconnect: this.options.onAgentConnect && this.options.onAgentConnect(),
|
||||
|
|
@ -157,61 +189,10 @@ export default class Assist {
|
|||
this.assistDemandedRestart = true
|
||||
this.app.stop();
|
||||
this.app.start().then(() => { this.assistDemandedRestart = false })
|
||||
const storedControllingAgent = sessionStorage.getItem(this.options.session_control_peer_key)
|
||||
if (storedControllingAgent !== null && ids.includes(storedControllingAgent)) {
|
||||
grantControl(storedControllingAgent)
|
||||
socket.emit("control_granted", storedControllingAgent)
|
||||
} else {
|
||||
sessionStorage.removeItem(this.options.session_control_peer_key)
|
||||
}
|
||||
|
||||
remoteControl.reconnect(ids)
|
||||
})
|
||||
|
||||
let confirmRC: ConfirmWindow | null = null
|
||||
const mouse = new Mouse() // TODO: lazy init
|
||||
let controllingAgent: string | null = null
|
||||
const requestControl = (id: string) => {
|
||||
if (controllingAgent !== null) {
|
||||
socket.emit("control_rejected", id)
|
||||
return
|
||||
}
|
||||
controllingAgent = id // TODO: more explicit pending state
|
||||
confirmRC = new ConfirmWindow(controlConfirmDefault(this.options.controlConfirm))
|
||||
confirmRC.mount().then(allowed => {
|
||||
if (allowed) {
|
||||
grantControl(id)
|
||||
socket.emit("control_granted", id)
|
||||
} else {
|
||||
releaseControl()
|
||||
socket.emit("control_rejected", id)
|
||||
}
|
||||
}).catch()
|
||||
}
|
||||
let onRemoteControlStop: (()=>void) | null = null
|
||||
const grantControl = (id: string) => {
|
||||
controllingAgent = id
|
||||
mouse.mount()
|
||||
onRemoteControlStop = this.options.onRemoteControlStart() || null
|
||||
sessionStorage.setItem(this.options.session_control_peer_key, id)
|
||||
}
|
||||
const releaseControl = () => {
|
||||
typeof onRemoteControlStop === 'function' && onRemoteControlStop()
|
||||
onRemoteControlStop = null
|
||||
confirmRC?.remove()
|
||||
mouse.remove()
|
||||
controllingAgent = null
|
||||
sessionStorage.removeItem(this.options.session_control_peer_key)
|
||||
}
|
||||
socket.on("request_control", requestControl)
|
||||
socket.on("release_control", (id: string) => {
|
||||
if (controllingAgent !== id) { return }
|
||||
releaseControl()
|
||||
})
|
||||
|
||||
|
||||
socket.on("scroll", (id, d) => { id === controllingAgent && mouse.scroll(d) })
|
||||
socket.on("click", (id, xy) => { id === controllingAgent && mouse.click(xy) })
|
||||
socket.on("move", (id, xy) => { id === controllingAgent && mouse.move(xy) })
|
||||
|
||||
let confirmCall:ConfirmWindow | null = null
|
||||
|
||||
socket.on("AGENT_DISCONNECTED", (id) => {
|
||||
|
|
@ -219,7 +200,7 @@ export default class Assist {
|
|||
this.agents[id] && this.agents[id].onDisconnect != null && this.agents[id].onDisconnect()
|
||||
delete this.agents[id]
|
||||
|
||||
controllingAgent === id && releaseControl()
|
||||
remoteControl.releaseControl(id)
|
||||
|
||||
// close the call also
|
||||
if (callingAgent === id) {
|
||||
|
|
@ -281,11 +262,20 @@ export default class Assist {
|
|||
style: this.options.confirmStyle,
|
||||
}))
|
||||
confirmAnswer = confirmCall.mount()
|
||||
this.playNotificationSound()
|
||||
this.onRemoteCallEnd = () => { // if call cancelled by a caller before confirmation
|
||||
app.debug.log("Received call_end during confirm window opened")
|
||||
confirmCall?.remove()
|
||||
setCallingState(CallingState.False)
|
||||
call.close()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (this.callingState !== CallingState.Requesting) { return }
|
||||
call.close()
|
||||
confirmCall?.remove()
|
||||
this.notifyCallEnd()
|
||||
setCallingState(CallingState.False)
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
confirmAnswer.then(agreed => {
|
||||
|
|
@ -296,13 +286,17 @@ export default class Assist {
|
|||
return
|
||||
}
|
||||
|
||||
let callUI = new CallWindow()
|
||||
const callUI = new CallWindow()
|
||||
annot = new AnnotationCanvas()
|
||||
annot.mount()
|
||||
callUI.setAssistentName(agentName)
|
||||
|
||||
const onCallEnd = this.options.onCallStart()
|
||||
const handleCallEnd = () => {
|
||||
call.close()
|
||||
callUI.remove()
|
||||
annot && annot.remove()
|
||||
annot = null
|
||||
setCallingState(CallingState.False)
|
||||
onCallEnd && onCallEnd()
|
||||
}
|
||||
|
|
@ -350,6 +344,16 @@ export default class Assist {
|
|||
});
|
||||
}
|
||||
|
||||
private playNotificationSound() {
|
||||
if ('Audio' in window) {
|
||||
new Audio("https://static.openreplay.com/tracker-assist/notification.mp3")
|
||||
.play()
|
||||
.catch(e => {
|
||||
this.app.debug.warn(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private clean() {
|
||||
if (this.peer) {
|
||||
this.peer.destroy()
|
||||
|
|
|
|||
|
|
@ -2,75 +2,94 @@ import type { Properties } from 'csstype';
|
|||
|
||||
import { declineCall, acceptCall, cross, remoteControl } from './icons.js'
|
||||
|
||||
type ButtonOptions = HTMLButtonElement | string | {
|
||||
innerHTML: string,
|
||||
style?: Properties,
|
||||
}
|
||||
const TEXT_GRANT_REMORTE_ACCESS = "Grant Remote Access";
|
||||
const TEXT_REJECT = "Reject";
|
||||
const TEXT_ANSWER_CALL = `${acceptCall}   Answer`;
|
||||
|
||||
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,
|
||||
text: string;
|
||||
style?: Properties;
|
||||
confirmBtn: ButtonOptions;
|
||||
declineBtn: ButtonOptions;
|
||||
}
|
||||
|
||||
export type Options = string | Partial<ConfirmWindowOptions>
|
||||
export type Options = string | Partial<ConfirmWindowOptions>;
|
||||
|
||||
function confirmDefault(
|
||||
opts: Options,
|
||||
confirmBtn: ButtonOptions,
|
||||
declineBtn: ButtonOptions,
|
||||
text: string,
|
||||
text: string
|
||||
): ConfirmWindowOptions {
|
||||
const isStr = typeof opts === "string"
|
||||
return Object.assign({
|
||||
text: isStr ? opts : text,
|
||||
confirmBtn,
|
||||
declineBtn,
|
||||
}, isStr ? undefined : opts)
|
||||
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?")
|
||||
export const callConfirmDefault = (opts: Options) =>
|
||||
confirmDefault(
|
||||
opts,
|
||||
TEXT_ANSWER_CALL,
|
||||
TEXT_REJECT,
|
||||
"You have an incoming call. Do you want to answer?"
|
||||
);
|
||||
export const controlConfirmDefault = (opts: Options) =>
|
||||
confirmDefault(
|
||||
opts,
|
||||
TEXT_GRANT_REMORTE_ACCESS,
|
||||
TEXT_REJECT,
|
||||
"Allow remote control?"
|
||||
);
|
||||
|
||||
function makeButton(options: ButtonOptions): HTMLButtonElement {
|
||||
if (options instanceof HTMLButtonElement) {
|
||||
return options
|
||||
return options;
|
||||
}
|
||||
const btn = document.createElement('button')
|
||||
const btn = document.createElement("button");
|
||||
Object.assign(btn.style, {
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
border: 0,
|
||||
padding: "10px 14px",
|
||||
fontSize: "14px",
|
||||
borderRadius: "3px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
borderRadius: "50%",
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
color: "white", // TODO: nice text button in case when only text is passed
|
||||
})
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textTransform: "uppercase",
|
||||
marginRight: "10px"
|
||||
});
|
||||
if (typeof options === "string") {
|
||||
btn.innerHTML = options
|
||||
btn.innerHTML = options;
|
||||
} else {
|
||||
btn.innerHTML = options.innerHTML
|
||||
Object.assign(btn.style, options.style)
|
||||
btn.innerHTML = options.innerHTML;
|
||||
Object.assign(btn.style, options.style);
|
||||
}
|
||||
return btn
|
||||
return btn;
|
||||
}
|
||||
|
||||
export default class ConfirmWindow {
|
||||
private wrapper: HTMLDivElement;
|
||||
|
||||
constructor(options: ConfirmWindowOptions) {
|
||||
const wrapper = document.createElement('div');
|
||||
const popup = document.createElement('div');
|
||||
const p = document.createElement('p');
|
||||
const wrapper = document.createElement("div");
|
||||
const popup = document.createElement("div");
|
||||
const p = document.createElement("p");
|
||||
p.innerText = options.text;
|
||||
const buttons = document.createElement('div');
|
||||
const buttons = document.createElement("div");
|
||||
const confirmBtn = makeButton(options.confirmBtn);
|
||||
const declineBtn = makeButton(options.declineBtn);
|
||||
buttons.appendChild(confirmBtn);
|
||||
|
|
@ -78,27 +97,45 @@ export default class ConfirmWindow {
|
|||
popup.appendChild(p);
|
||||
popup.appendChild(buttons);
|
||||
|
||||
Object.assign(confirmBtn.style, {
|
||||
background: "rgba(0, 167, 47, 1)",
|
||||
color: "white"
|
||||
});
|
||||
|
||||
Object.assign(declineBtn.style, {
|
||||
background: "#FFE9E9",
|
||||
color: "#CC0000"
|
||||
});
|
||||
|
||||
Object.assign(buttons.style, {
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-evenly",
|
||||
// justifyContent: "space-evenly",
|
||||
backgroundColor: "white",
|
||||
padding: "10px",
|
||||
boxShadow: "0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)",
|
||||
borderRadius: "6px"
|
||||
});
|
||||
|
||||
Object.assign(popup.style, {
|
||||
position: "relative",
|
||||
pointerEvents: "auto",
|
||||
margin: "4em auto",
|
||||
width: "90%",
|
||||
maxWidth: "400px",
|
||||
padding: "25px 30px",
|
||||
background: "black",
|
||||
opacity: ".75",
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
borderRadius: ".25em .25em .4em .4em",
|
||||
boxShadow: "0 0 20px rgb(0 0 0 / 20%)",
|
||||
}, options.style);
|
||||
Object.assign(
|
||||
popup.style,
|
||||
{
|
||||
font: "14px 'Roboto', sans-serif",
|
||||
position: "relative",
|
||||
pointerEvents: "auto",
|
||||
margin: "4em auto",
|
||||
width: "90%",
|
||||
maxWidth: "fit-content",
|
||||
padding: "20px",
|
||||
background: "#F3F3F3",
|
||||
//opacity: ".75",
|
||||
color: "black",
|
||||
borderRadius: "3px",
|
||||
boxShadow: "0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
options.style
|
||||
);
|
||||
|
||||
Object.assign(wrapper.style, {
|
||||
position: "fixed",
|
||||
|
|
@ -107,8 +144,8 @@ export default class ConfirmWindow {
|
|||
height: "100%",
|
||||
width: "100%",
|
||||
pointerEvents: "none",
|
||||
zIndex: 2147483647 - 1,
|
||||
})
|
||||
zIndex: 2147483647 - 1
|
||||
});
|
||||
|
||||
wrapper.appendChild(popup);
|
||||
this.wrapper = wrapper;
|
||||
|
|
@ -116,18 +153,19 @@ export default class ConfirmWindow {
|
|||
confirmBtn.onclick = () => {
|
||||
this._remove();
|
||||
this.resolve(true);
|
||||
}
|
||||
};
|
||||
declineBtn.onclick = () => {
|
||||
this._remove();
|
||||
this.resolve(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private resolve: (result: boolean) => void = ()=>{};
|
||||
private reject: ()=>void = ()=>{};
|
||||
private resolve: (result: boolean) => void = () => {};
|
||||
private reject: () => void = () => {};
|
||||
|
||||
mount(): Promise<boolean> {
|
||||
document.body.appendChild(this.wrapper);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
|
|
@ -135,7 +173,9 @@ export default class ConfirmWindow {
|
|||
}
|
||||
|
||||
private _remove() {
|
||||
if (!this.wrapper.parentElement) { return; }
|
||||
if (!this.wrapper.parentElement) {
|
||||
return;
|
||||
}
|
||||
document.body.removeChild(this.wrapper);
|
||||
}
|
||||
remove() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ export default class Mouse {
|
|||
if (el instanceof HTMLElement) {
|
||||
el.click()
|
||||
el.focus()
|
||||
return el
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private readonly pScrEl = document.scrollingElement || document.documentElement // Is it always correct
|
||||
|
|
|
|||
88
tracker/tracker-assist/src/RemoteControl.ts
Normal file
88
tracker/tracker-assist/src/RemoteControl.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import Mouse from './Mouse.js';
|
||||
import ConfirmWindow, { controlConfirmDefault } from './ConfirmWindow.js';
|
||||
import type { Options as AssistOptions } from './Assist'
|
||||
|
||||
enum RCStatus {
|
||||
Disabled,
|
||||
Requesting,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
export default class RemoteControl {
|
||||
private mouse: Mouse | null
|
||||
private status: RCStatus = RCStatus.Disabled
|
||||
private agentID: string | null = null
|
||||
|
||||
constructor(
|
||||
private options: AssistOptions,
|
||||
private onGrand: (sting?) => void,
|
||||
private onRelease: (sting?) => void) {}
|
||||
|
||||
reconnect(ids: string[]) {
|
||||
const storedID = sessionStorage.getItem(this.options.session_control_peer_key)
|
||||
if (storedID !== null && ids.includes(storedID)) {
|
||||
this.grantControl(storedID)
|
||||
} else {
|
||||
sessionStorage.removeItem(this.options.session_control_peer_key)
|
||||
}
|
||||
}
|
||||
|
||||
private confirm: ConfirmWindow | null = null
|
||||
requestControl = (id: string) => {
|
||||
if (this.agentID !== null) {
|
||||
this.releaseControl(id)
|
||||
return
|
||||
}
|
||||
setTimeout(() =>{
|
||||
if (this.status === RCStatus.Requesting) {
|
||||
this.releaseControl(id)
|
||||
}
|
||||
}, 30000)
|
||||
this.agentID = id
|
||||
this.status = RCStatus.Requesting
|
||||
this.confirm = new ConfirmWindow(controlConfirmDefault(this.options.controlConfirm))
|
||||
this.confirm.mount().then(allowed => {
|
||||
if (allowed) {
|
||||
this.grantControl(id)
|
||||
} else {
|
||||
this.releaseControl(id)
|
||||
}
|
||||
}).catch()
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
releaseControl = (id: string) => {
|
||||
if (this.agentID !== id) { return }
|
||||
this.confirm?.remove()
|
||||
this.mouse?.remove()
|
||||
this.mouse = null
|
||||
this.status = RCStatus.Disabled
|
||||
this.agentID = null
|
||||
sessionStorage.removeItem(this.options.session_control_peer_key)
|
||||
this.onRelease(id)
|
||||
}
|
||||
|
||||
scroll = (id, d) => { id === this.agentID && this.mouse?.scroll(d) }
|
||||
move = (id, xy) => { id === this.agentID && this.mouse?.move(xy) }
|
||||
private focused: HTMLElement | null = null
|
||||
click = (id, xy) => {
|
||||
if (id !== this.agentID || !this.mouse) { return }
|
||||
this.focused = this.mouse.click(xy)
|
||||
}
|
||||
input = (id, value) => {
|
||||
if (id !== this.agentID || !this.mouse || !this.focused) { return }
|
||||
if (this.focused instanceof HTMLTextAreaElement
|
||||
|| this.focused instanceof HTMLInputElement) {
|
||||
this.focused.value = value
|
||||
} else if (this.focused.isContentEditable) {
|
||||
this.focused.innerText = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
// 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 declineCall = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-telephone" viewBox="0 0 16 16">
|
||||
<path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
|
||||
</svg>`;
|
||||
|
||||
export const acceptCall = declineCall.replace('fill="#ef5261"', 'fill="green"')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue