@lightworkai.official/debug-capture 0.19.4 → 0.19.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/config.d.ts +17 -2
- package/dist/debug-capture.js +19 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +26 -26
- package/dist/ui/bubblePlace.d.ts +104 -0
- package/dist/ui/chatBubble.d.ts +15 -0
- package/package.json +1 -1
- package/src/config.ts +17 -2
- package/src/index.ts +1 -1
- package/src/ui/bubblePlace.ts +258 -0
- package/src/ui/chatBubble.ts +257 -34
package/src/ui/chatBubble.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* sign-in the panel asks (`hello`); the host's token goes to it by postMessage, to the support host's origin only
|
|
11
11
|
* แจ้งปัญหา… in the panel: the panel folds and this library's report opens — its screen, its page — linked to the chat
|
|
12
12
|
* the corner the call's card and frame sit above the bubble (`--lw-chat-lift`)
|
|
13
|
+
* its place out of the page's own floating controls, and where the desk says for a page (0.19.6, ui/bubblePlace)
|
|
13
14
|
*
|
|
14
15
|
* Shown only for a signed-in person (`identity` answers a token): a chat is someone's, and is read back.
|
|
15
16
|
*/
|
|
@@ -17,6 +18,7 @@ import { chatPlace, getConfig } from "../config";
|
|
|
17
18
|
import { hostToken, onLiveFrame } from "../live";
|
|
18
19
|
import { openSupportPortal } from "../portal";
|
|
19
20
|
import { launch } from "../reporter";
|
|
21
|
+
import { blockedRanges, placeFor, scanColumns, settleSpot, type PlaceRule, type Side } from "./bubblePlace";
|
|
20
22
|
import { followHostFont, OWN_FONTS } from "./hostFont";
|
|
21
23
|
import { strings } from "./strings";
|
|
22
24
|
|
|
@@ -33,11 +35,11 @@ const CSS = `
|
|
|
33
35
|
.bubble {
|
|
34
36
|
position: fixed; z-index: 2147483000; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%;
|
|
35
37
|
display: grid; place-items: center; border: 0; cursor: pointer; padding: 0;
|
|
36
|
-
background: #00468f; color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
|
|
38
|
+
background: var(--lw-chat-color, #00468f); color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
|
|
37
39
|
transition: transform 150ms ease, background 150ms ease;
|
|
38
40
|
font-family: var(--lw-host-font, ${OWN_FONTS});
|
|
39
41
|
}
|
|
40
|
-
.bubble:hover {
|
|
42
|
+
.bubble:hover { filter: brightness(0.92); transform: translateY(-1px); }
|
|
41
43
|
.bubble { touch-action: none; }
|
|
42
44
|
.bubble.dragging { cursor: grabbing; transform: scale(1.06); transition: none; box-shadow: 0 18px 35px -8px rgba(0, 0, 0, 0.4); }
|
|
43
45
|
.bubble:focus-visible { outline: 3px solid rgba(0, 70, 143, 0.35); outline-offset: 3px; }
|
|
@@ -65,7 +67,7 @@ const CSS = `
|
|
|
65
67
|
.label .text { display: block; max-width: min(240px, calc(100vw - ${SIZE + 64}px)); overflow: hidden; text-overflow: ellipsis; }
|
|
66
68
|
.at-left .label { right: auto; left: calc(100% + 12px); transform: translate(-4px, -50%); }
|
|
67
69
|
.at-left .label::after { right: auto; left: -4px; }
|
|
68
|
-
.new .label { color: #00468f; }
|
|
70
|
+
.new .label { color: var(--lw-chat-color, #00468f); }
|
|
69
71
|
.labelled .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); }
|
|
70
72
|
@media (hover: hover) { .bubble:hover .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); } }
|
|
71
73
|
.unlabelled .label, .bubble.dragging .label, .open .label { display: none; }
|
|
@@ -207,37 +209,100 @@ let opened = false;
|
|
|
207
209
|
function setLabel(): void {
|
|
208
210
|
if (!label || !bubble) return;
|
|
209
211
|
const config = getConfig();
|
|
210
|
-
|
|
212
|
+
// The host's own words in code win; else the desk's (ปุ่มแชท, its look); else the shipped ones.
|
|
213
|
+
const hostOwn = config.chat && config.chat !== true ? config.chat.label : undefined;
|
|
214
|
+
const own = hostOwn !== undefined ? hostOwn : deskLook?.label;
|
|
211
215
|
const l = bubbleLabel({ own, unread, opened, open: isOpen }, strings(config.locale));
|
|
212
216
|
bubble.classList.toggle("unlabelled", !l);
|
|
213
217
|
bubble.classList.toggle("labelled", !!l?.shown);
|
|
214
218
|
bubble.classList.toggle("new", !!l?.isNew);
|
|
215
|
-
bubble.classList.toggle("at-left", spot.side === "left");
|
|
219
|
+
bubble.classList.toggle("at-left", (shown ?? spot).side === "left");
|
|
216
220
|
label.querySelector(".text")!.textContent = l?.text ?? "";
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
/*
|
|
224
|
+
* The desk's look (0.19.5, the support desk's ปุ่มแชท — docs/CHAT-BUBBLE-PLAN.md): its face (or the chat glyph on its
|
|
225
|
+
* colour), its colour, the words beside the button — set on the desk, read with the first answer here.
|
|
226
|
+
*/
|
|
227
|
+
export interface DeskLook {
|
|
228
|
+
label?: string;
|
|
229
|
+
color?: string;
|
|
230
|
+
/** A path on the support host; null: the chat glyph on the colour. */
|
|
231
|
+
faceUrl?: string | null;
|
|
232
|
+
/** Step aside from the page's own floating controls (0.19.6) — the desk's switch; on unless false. */
|
|
233
|
+
avoid?: boolean;
|
|
234
|
+
/** A page's own place (0.19.6): hidden there, or started in a corner at a height — the desk's rules. */
|
|
235
|
+
places?: PlaceRule[];
|
|
236
|
+
}
|
|
237
|
+
let deskLook: DeskLook | null = null;
|
|
238
|
+
/** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
|
|
239
|
+
export function lookFaceUrl(host: string, look: DeskLook | null): string | null {
|
|
240
|
+
if (!look || look.faceUrl === undefined) return supportFaceUrl(host);
|
|
241
|
+
if (look.faceUrl === null) return null;
|
|
242
|
+
try {
|
|
243
|
+
return new URL(look.faceUrl.replace(/^\//, ""), base(host)).toString();
|
|
244
|
+
} catch {
|
|
245
|
+
return supportFaceUrl(host);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function wearLook(look: DeskLook | null): void {
|
|
249
|
+
deskLook = look;
|
|
250
|
+
if (!root || !bubble) return;
|
|
251
|
+
const color = look?.color && /^#[0-9a-f]{6}$/i.test(look.color) ? look.color : null;
|
|
252
|
+
if (color) root.style.setProperty("--lw-chat-color", color);
|
|
253
|
+
else root.style.removeProperty("--lw-chat-color");
|
|
254
|
+
const face = shadow?.querySelector<HTMLImageElement>(".face");
|
|
255
|
+
const url = lookFaceUrl(getConfig().host, look);
|
|
256
|
+
// Its rules and its switch may have changed where it stands.
|
|
257
|
+
queueRescan(true);
|
|
258
|
+
if (face) {
|
|
259
|
+
if (!url) {
|
|
260
|
+
bubble.classList.remove("has-face");
|
|
261
|
+
face.removeAttribute("src");
|
|
262
|
+
} else if (face.getAttribute("src") !== url) {
|
|
263
|
+
bubble.classList.remove("has-face");
|
|
264
|
+
face.src = url;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
setLabel();
|
|
268
|
+
}
|
|
269
|
+
|
|
219
270
|
let asking: Promise<void> | null = null;
|
|
220
271
|
/** What waits for the person, read with the host's token — no token: nobody is signed in, and there is no bubble. */
|
|
221
272
|
async function refreshUnread(): Promise<void> {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
273
|
+
if (asking) return asking;
|
|
274
|
+
/*
|
|
275
|
+
* Kept until it settles, then let go — by THIS read only. (`asking ??= (async () => { … finally { asking = null } })()`
|
|
276
|
+
* lost the let-go when the read ended before its first await — `show` saying no: the finally ran before the
|
|
277
|
+
* assignment, and every read after was that settled one, never asked again.)
|
|
278
|
+
*/
|
|
279
|
+
const run = readUnread();
|
|
280
|
+
asking = run;
|
|
281
|
+
try {
|
|
282
|
+
await run;
|
|
283
|
+
} finally {
|
|
284
|
+
if (asking === run) asking = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function readUnread(): Promise<void> {
|
|
288
|
+
try {
|
|
289
|
+
const config = getConfig();
|
|
290
|
+
// The host's own say (0.19.5): not for whoever is signed in now — no bubble, nothing asked.
|
|
291
|
+
const opts = config.chat && config.chat !== true ? config.chat : null;
|
|
292
|
+
if (opts?.show && !opts.show()) return void show(false);
|
|
293
|
+
const token = await hostToken();
|
|
294
|
+
if (!token) return void show(false);
|
|
295
|
+
const res = await fetch(new URL("ingest/support-unread", base(config.host)).toString(), { headers: { "x-realm-key": config.realmKey, authorization: `Bearer ${token}` } });
|
|
296
|
+
// A support host from before the chat, a desk that turned its bubble off, or a token it does not accept: no bubble.
|
|
297
|
+
if (res.status === 404 || res.status === 401 || res.status === 403) return void show(false);
|
|
298
|
+
if (!res.ok) return;
|
|
299
|
+
const out = (await res.json()) as { total?: number; look?: DeskLook };
|
|
300
|
+
if (out.look && typeof out.look === "object") wearLook(out.look);
|
|
301
|
+
show(true);
|
|
302
|
+
if (!isOpen) setBadge(typeof out.total === "number" ? out.total : 0);
|
|
303
|
+
} catch {
|
|
304
|
+
/* the next nudge asks again */
|
|
305
|
+
}
|
|
241
306
|
}
|
|
242
307
|
|
|
243
308
|
function show(on: boolean): void {
|
|
@@ -246,6 +311,7 @@ function show(on: boolean): void {
|
|
|
246
311
|
// Its words in the host page's own font — read again here, after the host has drawn itself.
|
|
247
312
|
if (on) followHostFont(root);
|
|
248
313
|
layout();
|
|
314
|
+
if (on) queueRescan(true);
|
|
249
315
|
}
|
|
250
316
|
|
|
251
317
|
/* ── Where it is: the config's place, or where the person put it ───────── */
|
|
@@ -282,24 +348,154 @@ function savePlace(): void {
|
|
|
282
348
|
}
|
|
283
349
|
}
|
|
284
350
|
|
|
351
|
+
/* ── Out of the page's way, and a page's own place (0.19.6, ui/bubblePlace) ─────── */
|
|
352
|
+
|
|
353
|
+
/** The host page's path — through `window`, as every other read here is. */
|
|
354
|
+
const pagePath = () => window.location?.pathname ?? "/";
|
|
355
|
+
/** Where it stands now — `spot` (or the page's rule), moved clear of the page's floating controls. */
|
|
356
|
+
let shown: BubbleSpot | null = null;
|
|
357
|
+
/** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (settleSpot). */
|
|
358
|
+
let shownFor = "";
|
|
359
|
+
/** The heights each side's column rules out, as last read off the page — null: not read yet, or not stepping aside. */
|
|
360
|
+
let blocked: Record<Side, [number, number][]> | null = null;
|
|
361
|
+
/** Hidden on this page by the desk's rule (ซ่อนปุ่มแชทในหน้านี้). */
|
|
362
|
+
let ruleHidden = false;
|
|
363
|
+
|
|
364
|
+
function avoidSetting(): { on: boolean; extra: string | null } {
|
|
365
|
+
const c = getConfig().chat;
|
|
366
|
+
const own = c && c !== true ? c.avoid : undefined;
|
|
367
|
+
if (own === false || deskLook?.avoid === false) return { on: false, extra: null };
|
|
368
|
+
return { on: true, extra: typeof own === "string" && own.trim() ? own.trim() : null };
|
|
369
|
+
}
|
|
370
|
+
/** Where it wants to be on this page: put by hand — kept; else the desk's rule for the page; else the config's place. */
|
|
371
|
+
function wanted(): BubbleSpot {
|
|
372
|
+
const rule = placeFor(pagePath(), deskLook?.places);
|
|
373
|
+
ruleHidden = rule?.show === "hide";
|
|
374
|
+
if (movedByHand || !rule || rule.show === "hide") return spot;
|
|
375
|
+
return { side: rule.show, x: spot.x, y: rule.y };
|
|
376
|
+
}
|
|
377
|
+
/** Read the page's floating controls across both corners' columns — then place again. */
|
|
378
|
+
function rescan(): void {
|
|
379
|
+
if (!bubble) return;
|
|
380
|
+
const { on, extra } = avoidSetting();
|
|
381
|
+
if (!on) {
|
|
382
|
+
blocked = null;
|
|
383
|
+
return layout();
|
|
384
|
+
}
|
|
385
|
+
const v = view();
|
|
386
|
+
const x = spot.x;
|
|
387
|
+
const colR = { left: v.w - x - SIZE, right: v.w - x };
|
|
388
|
+
const colL = { left: x, right: x + SIZE };
|
|
389
|
+
const [foundR = [], foundL = []] = scanColumns([colR, colL], v, { extra });
|
|
390
|
+
const right = blockedRanges(foundR, colR, v.h, SIZE);
|
|
391
|
+
const left = blockedRanges(foundL, colL, v.h, SIZE);
|
|
392
|
+
// Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
|
|
393
|
+
calm = JSON.stringify([right, left]) === JSON.stringify(blocked && [blocked.right, blocked.left]) ? Math.min(calm + 1, CALM_STEPS) : 0;
|
|
394
|
+
blocked = { right, left };
|
|
395
|
+
layout();
|
|
396
|
+
}
|
|
397
|
+
/*
|
|
398
|
+
* How often the page is read (a read is two columns of hit tests, a few ms). A page that keeps changing — a clock, a
|
|
399
|
+
* live list — said "look again" ten times a second, and a read every 400 ms cost it half a second of script in ten
|
|
400
|
+
* (0.19.5: 20 ms). So: a read that finds nothing new doubles the wait for the next one a change asks for (400 ms up to
|
|
401
|
+
* 3.2 s); a person's own press or key, a new page, a resize or the tab coming back is looked at at once (URGENT_MS),
|
|
402
|
+
* and resets it. Never while the panel is open (it does not move then), while it is dragged, or in a hidden tab.
|
|
403
|
+
*/
|
|
404
|
+
const RESCAN_MS = 400;
|
|
405
|
+
const CALM_STEPS = 3;
|
|
406
|
+
const URGENT_MS = 60;
|
|
407
|
+
let rescanTimer: ReturnType<typeof setTimeout> | undefined;
|
|
408
|
+
let rescanDue = 0;
|
|
409
|
+
let lastRescan = 0;
|
|
410
|
+
let calm = 0;
|
|
411
|
+
function queueRescan(urgent = false): void {
|
|
412
|
+
if (!root) return;
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
if (urgent) {
|
|
415
|
+
calm = 0;
|
|
416
|
+
if (rescanTimer && rescanDue - now <= URGENT_MS) return;
|
|
417
|
+
clearTimeout(rescanTimer);
|
|
418
|
+
rescanTimer = undefined;
|
|
419
|
+
}
|
|
420
|
+
if (rescanTimer) return;
|
|
421
|
+
const wait = urgent ? URGENT_MS : Math.max(0, RESCAN_MS * 2 ** calm - (now - lastRescan));
|
|
422
|
+
rescanDue = now + wait;
|
|
423
|
+
rescanTimer = setTimeout(() => {
|
|
424
|
+
rescanTimer = undefined;
|
|
425
|
+
if (!root || isOpen || pressed?.moved || (typeof document !== "undefined" && document.hidden)) return;
|
|
426
|
+
lastRescan = Date.now();
|
|
427
|
+
rescan();
|
|
428
|
+
}, wait);
|
|
429
|
+
}
|
|
430
|
+
/** The page's own changes worth a look: a toolbar opening, a page arriving — never this library's own boxes. */
|
|
431
|
+
let watcher: MutationObserver | null = null;
|
|
432
|
+
let lastPath = "";
|
|
433
|
+
let pathTimer: ReturnType<typeof setInterval> | undefined;
|
|
434
|
+
const onPageMoved = () => queueRescan();
|
|
435
|
+
/** What a person did, or the window itself changed: looked at at once. */
|
|
436
|
+
const onPageActed = () => queueRescan(true);
|
|
437
|
+
const ACTED = ["popstate", "hashchange", "pointerup", "keyup"] as const;
|
|
438
|
+
function watchPage(on: boolean): void {
|
|
439
|
+
watcher?.disconnect();
|
|
440
|
+
watcher = null;
|
|
441
|
+
clearInterval(pathTimer);
|
|
442
|
+
for (const t of ACTED) window.removeEventListener(t, onPageActed, true);
|
|
443
|
+
for (const t of ["scroll", "transitionend", "animationend"] as const) window.removeEventListener(t, onPageMoved, true);
|
|
444
|
+
document.removeEventListener("visibilitychange", onPageActed);
|
|
445
|
+
if (!on) return;
|
|
446
|
+
if (typeof MutationObserver !== "undefined") {
|
|
447
|
+
watcher = new MutationObserver((records) => {
|
|
448
|
+
const theirs = records.some((r) => {
|
|
449
|
+
const t = r.target as Element;
|
|
450
|
+
if (t === document.documentElement && r.type === "attributes") return false;
|
|
451
|
+
return !(t.nodeType === 1 && /^LW-/.test(t.tagName)) && t !== root;
|
|
452
|
+
});
|
|
453
|
+
if (theirs) queueRescan();
|
|
454
|
+
});
|
|
455
|
+
watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"] });
|
|
456
|
+
}
|
|
457
|
+
for (const t of ACTED) window.addEventListener(t, onPageActed, { capture: true, passive: true });
|
|
458
|
+
// A toolbar's own opening ends after the press; a sticky bar sticks as the page scrolls.
|
|
459
|
+
for (const t of ["scroll", "transitionend", "animationend"] as const) window.addEventListener(t, onPageMoved, { capture: true, passive: true });
|
|
460
|
+
document.addEventListener("visibilitychange", onPageActed);
|
|
461
|
+
// A single-page app moves by history.pushState, which says nothing: the path is looked at once a second.
|
|
462
|
+
lastPath = pagePath();
|
|
463
|
+
pathTimer = setInterval(() => {
|
|
464
|
+
if (pagePath() === lastPath) return;
|
|
465
|
+
lastPath = pagePath();
|
|
466
|
+
layout();
|
|
467
|
+
queueRescan(true);
|
|
468
|
+
}, 1000);
|
|
469
|
+
}
|
|
470
|
+
|
|
285
471
|
/** Everything placed from `spot` and `panelAt`, inside the window as it is now — and the call's card clear of the bubble. */
|
|
286
472
|
function layout(): void {
|
|
287
473
|
if (!bubble || !panel) return;
|
|
288
474
|
const v = view();
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
475
|
+
const want = wanted();
|
|
476
|
+
const wantY = clampBubbleY(want.y, v.h);
|
|
477
|
+
// While the panel is open it stays where it was opened from — never moved under the person's eyes.
|
|
478
|
+
const wish = `${pagePath()}|${want.side}|${want.x}|${wantY}`;
|
|
479
|
+
const prev = shown && shownFor === wish ? shown : null;
|
|
480
|
+
const free = !isOpen || !shown ? (blocked && avoidSetting().on ? settleSpot(prev, { side: want.side, y: wantY }, blocked, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN)) : { side: want.side, y: wantY }) : shown;
|
|
481
|
+
shown = { side: free.side, x: want.x, y: free.y };
|
|
482
|
+
shownFor = wish;
|
|
483
|
+
const at = shown;
|
|
484
|
+
const y = at.y;
|
|
485
|
+
const other = at.side === "right" ? "left" : "right";
|
|
486
|
+
bubble.style.display = ruleHidden && !isOpen ? "none" : "";
|
|
487
|
+
Object.assign(bubble.style, { [at.side]: `${at.x}px`, [other]: "", bottom: `${y}px`, top: "" });
|
|
292
488
|
if (panelAt) {
|
|
293
489
|
// The height it had when it was picked up — a drag moves it, never resizes it under the hand.
|
|
294
490
|
const box = { w: Math.min(PANEL_W, v.w - 32), h: Math.min(panelAt.h, v.h - 2 * MARGIN) };
|
|
295
491
|
const p = clampPanel(panelAt, box, v);
|
|
296
492
|
Object.assign(panel.style, { left: `${p.x}px`, top: `${p.y}px`, right: "", bottom: "", height: `${box.h}px` });
|
|
297
493
|
} else {
|
|
298
|
-
const a = anchorPanel(
|
|
299
|
-
Object.assign(panel.style, { [
|
|
494
|
+
const a = anchorPanel(at, v.h);
|
|
495
|
+
Object.assign(panel.style, { [at.side]: `${at.x}px`, [other]: "", bottom: a.bottom !== undefined ? `${a.bottom}px` : "", top: a.top !== undefined ? `${a.top}px` : "", height: `${a.height}px` });
|
|
300
496
|
}
|
|
301
497
|
// The call's card and frame sit above the bubble while it is there and low on their side (ring.ts, callFrame.ts).
|
|
302
|
-
const lift = root && root.style.display !== "none" ? liftFor(
|
|
498
|
+
const lift = root && root.style.display !== "none" && bubble.style.display !== "none" ? liftFor(at) : 0;
|
|
303
499
|
if (lift) document.documentElement.style.setProperty("--lw-chat-lift", `${lift}px`);
|
|
304
500
|
else document.documentElement.style.removeProperty("--lw-chat-lift");
|
|
305
501
|
setLabel();
|
|
@@ -341,7 +537,9 @@ function onBubbleUp(e: PointerEvent): void {
|
|
|
341
537
|
panelAt = null;
|
|
342
538
|
savePlace();
|
|
343
539
|
}
|
|
344
|
-
|
|
540
|
+
// Let go on a toolbar: to the nearest free place, read off the page as it is now.
|
|
541
|
+
if (avoidSetting().on) rescan();
|
|
542
|
+
else layout();
|
|
345
543
|
}
|
|
346
544
|
|
|
347
545
|
/* ── The panel, dragged by its head — the press is in the frame (as the call's, ui/callFrame) ─── */
|
|
@@ -386,7 +584,10 @@ function followHere(on: boolean): void {
|
|
|
386
584
|
};
|
|
387
585
|
for (const t of ["pointermove", "pointerup", "pointercancel"] as const) window.addEventListener(t, hostFollow, true);
|
|
388
586
|
}
|
|
389
|
-
const onResize = () =>
|
|
587
|
+
const onResize = () => {
|
|
588
|
+
layout();
|
|
589
|
+
queueRescan(true);
|
|
590
|
+
};
|
|
390
591
|
|
|
391
592
|
function post(message: Record<string, unknown>): void {
|
|
392
593
|
frame?.contentWindow?.postMessage({ source: HOST, ...message }, hostOrigin());
|
|
@@ -462,6 +663,9 @@ export function closeSupportChat(refocus = true): void {
|
|
|
462
663
|
post({ type: "hidden" });
|
|
463
664
|
if (refocus) bubble?.focus();
|
|
464
665
|
void refreshUnread();
|
|
666
|
+
// Closed: free to step aside again from whatever the page did meanwhile.
|
|
667
|
+
layout();
|
|
668
|
+
queueRescan(true);
|
|
465
669
|
}
|
|
466
670
|
|
|
467
671
|
let stopFrames: (() => void) | null = null;
|
|
@@ -491,8 +695,12 @@ export function mountSupportChat(): void {
|
|
|
491
695
|
// The desk's face (the owner, 10-31: "all the support icon that user see … use this icon"): the support host's own,
|
|
492
696
|
// the one its replies wear — the chat glyph until it is there, and if it never comes.
|
|
493
697
|
const face = shadow.querySelector<HTMLImageElement>(".face")!;
|
|
494
|
-
|
|
495
|
-
face.
|
|
698
|
+
// Every picture it is given (the desk's look may change it, 0.19.5): worn once loaded, the glyph if it fails.
|
|
699
|
+
face.addEventListener("load", () => {
|
|
700
|
+
if (face.getAttribute("src")) bubble?.classList.add("has-face");
|
|
701
|
+
});
|
|
702
|
+
face.addEventListener("error", () => bubble?.classList.remove("has-face"));
|
|
703
|
+
face.src = lookFaceUrl(config.host, deskLook) ?? supportFaceUrl(config.host);
|
|
496
704
|
// Where the person left it on this device, else the config's place (0.19.2).
|
|
497
705
|
loadPlace();
|
|
498
706
|
layout();
|
|
@@ -510,6 +718,7 @@ export function mountSupportChat(): void {
|
|
|
510
718
|
document.addEventListener("keydown", onKey);
|
|
511
719
|
window.addEventListener("focus", onFocus);
|
|
512
720
|
window.addEventListener("resize", onResize);
|
|
721
|
+
watchPage(true);
|
|
513
722
|
// The desk wrote, or opened a เรื่อง, or read: the badge asks again — while the panel is open, the panel tells it.
|
|
514
723
|
let settle: ReturnType<typeof setTimeout> | undefined;
|
|
515
724
|
stopFrames = onLiveFrame(() => {
|
|
@@ -521,13 +730,27 @@ export function mountSupportChat(): void {
|
|
|
521
730
|
}
|
|
522
731
|
|
|
523
732
|
/** Take it away — for a host that unmounts, and for the tests. */
|
|
733
|
+
/** Ask again whether there is a bubble, and what waits (0.19.5) — after a sign-in or a sign-out, or `show` changed. */
|
|
734
|
+
export function refreshSupportChat(): Promise<void> {
|
|
735
|
+
return root ? refreshUnread() : Promise.resolve();
|
|
736
|
+
}
|
|
737
|
+
|
|
524
738
|
export function unmountSupportChat(): void {
|
|
739
|
+
deskLook = null;
|
|
525
740
|
stopFrames?.();
|
|
526
741
|
stopFrames = null;
|
|
527
742
|
window.removeEventListener("message", onMessage);
|
|
528
743
|
document.removeEventListener("keydown", onKey);
|
|
529
744
|
window.removeEventListener("focus", onFocus);
|
|
530
745
|
window.removeEventListener("resize", onResize);
|
|
746
|
+
watchPage(false);
|
|
747
|
+
clearTimeout(rescanTimer);
|
|
748
|
+
rescanTimer = undefined;
|
|
749
|
+
calm = 0;
|
|
750
|
+
shown = null;
|
|
751
|
+
shownFor = "";
|
|
752
|
+
blocked = null;
|
|
753
|
+
ruleHidden = false;
|
|
531
754
|
followHere(false);
|
|
532
755
|
root?.remove();
|
|
533
756
|
root = shadow = panel = frame = bubble = badge = label = null;
|