@mrrocket/sdk 0.14.0 → 0.15.0

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.
@@ -1,5 +1,13 @@
1
1
  // src/messenger-ui.ts
2
- import { createApi, inkOn, replyLine } from "./api-client.js";
2
+ import {
3
+ BRAND_URL,
4
+ CHAT_FILES_PER_MESSAGE,
5
+ CHAT_FILE_MAX_BYTES,
6
+ CHAT_FILE_TYPES,
7
+ createApi,
8
+ inkOn,
9
+ replyLine
10
+ } from "./api-client.js";
3
11
  import { injectStyles } from "./styles-css.js";
4
12
  var POLL_OPEN_MS = 4e3;
5
13
  var POLL_WAITING_MS = 8e3;
@@ -61,8 +69,15 @@ var I = {
61
69
  dots: "M13 12a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM13 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM13 19a1 1 0 1 1-2 0 1 1 0 0 1 2 0z",
62
70
  expand: "M15 3h6v6M21 3l-7 7M3 21l7-7M9 21H3v-6",
63
71
  collapse: "m14 10 7-7M20 10h-6V4M3 21l7-7M4 14h6v6",
64
- minus: "M5 12h14"
72
+ minus: "M5 12h14",
73
+ down: "m6 9 6 6 6-6",
74
+ help: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01",
75
+ clip: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48",
76
+ image: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM9 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM21 15l-3.09-3.09a2 2 0 0 0-2.82 0L6 21",
77
+ screen: "M20 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM8 21h8M12 17v4"
65
78
  };
79
+ var lineOf = (m) => m.body || (m.attachments?.length === 1 ? `Sent ${m.attachments[0].name}` : m.attachments?.length ? `Sent ${m.attachments.length} files` : "");
80
+ var canShoot = () => typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
66
81
  var BUBBLE = "M7.9 20A9 9 0 1 0 4 16.1L2 22z";
67
82
  var filled = (d, size) => {
68
83
  const svg = document.createElementNS(SVG_NS, "svg");
@@ -73,6 +88,18 @@ var filled = (d, size) => {
73
88
  svg.appendChild(path);
74
89
  return svg;
75
90
  };
91
+ var duo = (shape, cut, size, width = 2) => {
92
+ const svg = filled(shape, size);
93
+ const path = document.createElementNS(SVG_NS, "path");
94
+ for (const [k, v] of Object.entries({ d: cut, class: "ckw-cut", fill: "none", "stroke-width": String(width), "stroke-linecap": "round", "stroke-linejoin": "round" })) path.setAttribute(k, v);
95
+ svg.appendChild(path);
96
+ return svg;
97
+ };
98
+ var DOTS = "M8 12h.01M12 12h.01M16 12h.01";
99
+ var HOUSE = "M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z";
100
+ var SMILE = "M9.5 15.5c1.4 1 3.6 1 5 0";
101
+ var DISC = "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z";
102
+ var QMARK = "M9.4 9.2a2.7 2.7 0 0 1 5.2 1c0 1.8-2.6 2.5-2.6 2.5M12 16.8h.01";
76
103
  var chime = /* @__PURE__ */ (() => {
77
104
  let ctx = null;
78
105
  const get = () => {
@@ -149,7 +176,7 @@ var mountMessenger = (o) => {
149
176
  const start = (look) => {
150
177
  if (real || gone) return;
151
178
  const defined = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
152
- real = build({ ...look ?? {}, ...defined, accent: o.accent ?? look?.accent, theme: o.theme ?? look?.theme });
179
+ real = build({ ...look ?? {}, ...defined, accent: o.accent ?? look?.accent, theme: o.theme ?? look?.theme, branding: look?.branding ?? true });
153
180
  for (const fn of queued.splice(0)) fn(real);
154
181
  };
155
182
  const timer = setTimeout(() => start(null), LOOK_WAIT_MS);
@@ -166,10 +193,11 @@ var mountMessenger = (o) => {
166
193
  }
167
194
  };
168
195
  };
196
+ var PLACES = ["top-left", "top-center", "top-right", "middle-left", "middle-right", "bottom-left", "bottom-center", "bottom-right"];
169
197
  var build = (o) => {
170
198
  injectStyles();
171
- const position = o.position ?? "bottom-right";
172
- const title = o.title || "Chat with us";
199
+ const position = PLACES.find((p) => p === o.position) ?? "bottom-right";
200
+ const title = o.title || "Support";
173
201
  const greeting = o.greeting || "Hi! How can we help?";
174
202
  const askEmail = o.askEmail ?? true;
175
203
  const label = o.label ?? "";
@@ -180,12 +208,19 @@ var build = (o) => {
180
208
  const soundOn = o.sound ?? true;
181
209
  const previewOn = o.preview ?? true;
182
210
  const api = () => o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() });
183
- const root = el("div", { class: "ckw-root", "data-theme": o.theme ?? "auto" });
211
+ const root = el("div", {
212
+ class: "ckw-root",
213
+ "data-theme": o.theme ?? "auto",
214
+ "data-corners": o.corners === "soft" || o.corners === "square" ? o.corners : "round",
215
+ "data-font": o.font === "system" ? "system" : "site",
216
+ "data-header": o.header === "color" ? "color" : "plain"
217
+ });
184
218
  if (o.container) root.setAttribute("data-contained", "");
185
219
  if (o.accent) {
186
220
  root.style.setProperty("--ckw-accent", o.accent);
187
221
  root.style.setProperty("--ckw-accent-ink", inkOn(o.accent));
188
222
  }
223
+ o.glow?.slice(0, 2).forEach((c, i) => /^#[0-9a-f]{6}$/i.test(c) && root.style.setProperty(`--ckw-glow${i + 2}`, c));
189
224
  (o.container ?? document.body).appendChild(root);
190
225
  let open = false;
191
226
  let view = "home";
@@ -234,6 +269,13 @@ var build = (o) => {
234
269
  if (id) writeSeen(id);
235
270
  };
236
271
  const avatar = (size) => el("span", { class: `ckw-msgr-av${size ? " ckw-msgr-av-sm" : ""}`, "aria-hidden": "true" }, initial);
272
+ const logo = el("span", { class: "ckw-msgr-logo", "aria-hidden": "true" });
273
+ if (o.logo) {
274
+ const img = el("img", { src: o.logo, alt: "" });
275
+ img.addEventListener("error", () => logo.replaceChildren(avatar("sm")));
276
+ logo.append(img);
277
+ } else logo.append(avatar("sm"));
278
+ const sky = el("div", { class: "ckw-msgr-sky", "aria-hidden": "true" });
237
279
  const leaving = /* @__PURE__ */ new Map();
238
280
  const enter = (node, parent = root) => {
239
281
  clearTimeout(leaving.get(node));
@@ -262,8 +304,8 @@ var build = (o) => {
262
304
  const fabIcons = el(
263
305
  "span",
264
306
  { class: "ckw-msgr-fab-ic", "aria-hidden": "true" },
265
- el("span", { class: "ckw-msgr-fab-i", "data-i": "chat" }, filled(BUBBLE, label ? 20 : 28)),
266
- el("span", { class: "ckw-msgr-fab-i", "data-i": "x" }, icon(I.cross, label ? 16 : 22))
307
+ el("span", { class: "ckw-msgr-fab-i", "data-i": "chat" }, duo(BUBBLE, DOTS, label ? 20 : 27, 2.6)),
308
+ el("span", { class: "ckw-msgr-fab-i", "data-i": "x" }, icon(I.down, label ? 18 : 24))
267
309
  );
268
310
  const fabCount = el("span", { class: "ckw-msgr-count", "aria-hidden": "true" });
269
311
  fab.append(fabIcons);
@@ -285,7 +327,7 @@ var build = (o) => {
285
327
  const m = unseen.at(-1);
286
328
  if (!previewShown() || !m) return leave(prev);
287
329
  hideTeaser(true);
288
- const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 12));
330
+ const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 14));
289
331
  x.addEventListener("click", (e) => {
290
332
  e.stopPropagation();
291
333
  previewGone = true;
@@ -294,12 +336,11 @@ var build = (o) => {
294
336
  const card = el(
295
337
  "button",
296
338
  { type: "button", class: "ckw-msgr-preview-card" },
297
- avatar(),
298
339
  el(
299
340
  "span",
300
341
  { class: "ckw-msgr-preview-main" },
301
342
  el("span", { class: "ckw-msgr-preview-who" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` \xB7 ${unseen.length} new` : ""}`),
302
- el("span", { class: "ckw-msgr-preview-text" }, m.body)
343
+ el("span", { class: "ckw-msgr-preview-text" }, lineOf(m))
303
344
  )
304
345
  );
305
346
  card.addEventListener("click", () => show("messages"));
@@ -311,7 +352,7 @@ var build = (o) => {
311
352
  const showTeaser = () => {
312
353
  clearTimeout(teaserTimer);
313
354
  if (open || previewShown() || !launcherOn) return;
314
- const chat = el("button", { type: "button", class: "ckw-msgr-teaser-b", "data-main": "" }, icon(I.chat), el("span", {}, "Chat with us"));
355
+ const chat = el("button", { type: "button", class: "ckw-msgr-teaser-b", "data-main": "" }, icon(I.chat), el("span", {}, "Chat"));
315
356
  chat.addEventListener("click", () => show("messages"));
316
357
  const buttons = [chat];
317
358
  if (helpOn && hasArticles) {
@@ -352,23 +393,24 @@ var build = (o) => {
352
393
  const body = el("div", { class: "ckw-pop-body ckw-msgr-body" });
353
394
  pop.addEventListener("click", () => chime.unlock());
354
395
  const lead = el("span", { class: "ckw-msgr-lead" });
355
- const seg = el("nav", { class: "ckw-msgr-seg", "aria-label": "Messenger" });
356
- const mark = el("span", { class: "ckw-msgr-seg-mark", "aria-hidden": "true" });
357
- const segTab = (v, name, d) => {
358
- const b = el("button", { type: "button", class: "ckw-msgr-seg-b" }, icon(d, 15), el("span", {}, name));
396
+ const mid = el("span", { class: "ckw-msgr-mid" });
397
+ const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": "Messenger" });
398
+ const tab = (v, name, line, shape, cut) => {
399
+ const b = el("button", { type: "button", class: "ckw-msgr-tab" }, el("span", { class: "ckw-msgr-tab-ic" }, icon(line, 22), duo(shape, cut, 22)), el("span", {}, name));
359
400
  b.addEventListener("click", () => {
360
401
  collection = null;
361
402
  go(v);
362
403
  });
363
404
  return b;
364
405
  };
365
- const chatTab = segTab("messages", "Chat", I.chat);
366
- const helpTab = segTab("help", "Help", I.book);
406
+ const homeTab = tab("home", "Home", I.home, HOUSE, SMILE);
407
+ const chatTab = tab("messages", "Messages", I.chat, BUBBLE, DOTS);
408
+ const helpTab = tab("help", "Help", I.help, DISC, QMARK);
367
409
  const tabCount = el("span", { class: "ckw-msgr-count ckw-msgr-count-tab", "aria-hidden": "true" });
368
410
  const menuBtn = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": "More", "aria-haspopup": "menu", "aria-expanded": "false" }, icon(I.dots, 18));
369
411
  const xBtn = el("button", { type: "button", class: "ckw-msgr-ib ckw-msgr-x", "aria-label": "Close messenger" }, icon(I.cross));
370
412
  xBtn.addEventListener("click", () => close());
371
- bar.append(lead, seg, el("span", { class: "ckw-msgr-trail" }, menuBtn, xBtn));
413
+ bar.append(lead, mid, el("span", { class: "ckw-msgr-trail" }, menuBtn, xBtn));
372
414
  const menu = el("div", { class: "ckw-msgr-menu", role: "menu" });
373
415
  const menuOpen = () => menu.isConnected && !menu.hasAttribute("data-out");
374
416
  const closeMenu = () => {
@@ -393,7 +435,6 @@ var build = (o) => {
393
435
  menuItem(wide ? I.collapse : I.expand, wide ? "Collapse view" : "Expand view", () => {
394
436
  wide = !wide;
395
437
  pop.toggleAttribute("data-wide", wide);
396
- requestAnimationFrame(placeMark);
397
438
  })
398
439
  );
399
440
  menuBtn.setAttribute("aria-expanded", "true");
@@ -401,16 +442,6 @@ var build = (o) => {
401
442
  menu.querySelector("button")?.focus();
402
443
  });
403
444
  pop.addEventListener("click", () => menuOpen() && closeMenu());
404
- function placeMark() {
405
- const current = view === "messages" ? chatTab : view !== "home" ? helpTab : null;
406
- if (!current || !current.isConnected) return mark.removeAttribute("data-on");
407
- const first = !mark.hasAttribute("data-on");
408
- if (first) mark.setAttribute("data-still", "");
409
- mark.style.width = `${current.offsetWidth}px`;
410
- mark.style.transform = `translateX(${current.offsetLeft}px)`;
411
- mark.setAttribute("data-on", "");
412
- if (first) requestAnimationFrame(() => mark.removeAttribute("data-still"));
413
- }
414
445
  const drawBar = () => {
415
446
  const back = (to, name) => {
416
447
  const b = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": name }, icon(I.back, 18));
@@ -428,18 +459,20 @@ var build = (o) => {
428
459
  go("help");
429
460
  }, "All collections")
430
461
  );
431
- else {
432
- const home = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": "Home", "aria-current": view === "home" ? "page" : "false" }, icon(I.home, 17));
433
- home.addEventListener("click", () => go("home"));
434
- lead.replaceChildren(home);
435
- }
436
- const tabsWanted = helpOn && hasArticles !== false ? [mark, chatTab, helpTab] : [mark, chatTab];
437
- if (seg.childNodes.length !== tabsWanted.length) seg.replaceChildren(...tabsWanted);
462
+ else if (view === "messages") lead.replaceChildren(back(() => go("home"), "Back to home"));
463
+ else if (view === "home") lead.replaceChildren(logo);
464
+ else lead.replaceChildren();
465
+ if (view === "messages") mid.replaceChildren(avatar("sm"), el("span", { class: "ckw-msgr-mid-t" }, el("b", {}, title), el("span", { class: "ckw-msgr-online" }, reply)));
466
+ else if (view === "help") mid.replaceChildren(el("b", {}, "Help"));
467
+ else mid.replaceChildren();
468
+ const items = helpOn && hasArticles !== false ? [homeTab, chatTab, helpTab] : [homeTab, chatTab];
469
+ if (tabs.childNodes.length !== items.length) tabs.replaceChildren(...items);
470
+ homeTab.setAttribute("aria-current", view === "home" ? "page" : "false");
438
471
  chatTab.setAttribute("aria-current", view === "messages" ? "page" : "false");
439
472
  helpTab.setAttribute("aria-current", view !== "home" && view !== "messages" ? "page" : "false");
440
473
  if (unseen.length && view !== "messages") {
441
474
  tabCount.textContent = String(Math.min(unseen.length, 9));
442
- if (!tabCount.isConnected) chatTab.append(tabCount);
475
+ if (!tabCount.isConnected) chatTab.firstChild.appendChild(tabCount);
443
476
  } else tabCount.remove();
444
477
  };
445
478
  const form = el("form", { class: "ckw-msgr-compose" });
@@ -448,13 +481,164 @@ var build = (o) => {
448
481
  const row = el("div", { class: "ckw-msgr-box" });
449
482
  const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write your message\u2026", "aria-label": "Message", maxlength: "4000" });
450
483
  const send = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": "Send" }, icon(I.up));
451
- row.append(text, el("div", { class: "ckw-msgr-box-foot" }, el("span", { class: "ckw-msgr-hint" }, "Enter to send"), send));
484
+ const shots = canShoot();
485
+ const clip = el(
486
+ "button",
487
+ { type: "button", class: "ckw-msgr-clip", "aria-label": shots ? "Attach a file or a screenshot" : "Attach a file", ...shots ? { "aria-haspopup": "menu", "aria-expanded": "false" } : {} },
488
+ icon(I.clip, 18)
489
+ );
490
+ row.append(text, el("div", { class: "ckw-msgr-tools" }, clip, el("span", { class: "ckw-msgr-sp" }), send));
452
491
  row.addEventListener("click", (e) => e.target === row && text.focus());
453
492
  form.append(row);
493
+ const picker = el("input", { type: "file", accept: CHAT_FILE_TYPES.join(","), multiple: "", hidden: "", tabindex: "-1", "aria-hidden": "true" });
494
+ form.append(picker);
495
+ const tray = el("div", { class: "ckw-msgr-files", role: "list", "aria-label": "Files to send" });
496
+ let picked = [];
497
+ const localUrls = /* @__PURE__ */ new Map();
498
+ const fileUrl = (id) => localUrls.get(id) ?? api().attachmentUrl(id);
499
+ const forget = (p) => {
500
+ p.ctrl.abort();
501
+ if (![...localUrls.values()].includes(p.local)) URL.revokeObjectURL(p.local);
502
+ };
503
+ const dropFile = (p) => {
504
+ picked = picked.filter((x) => x !== p);
505
+ forget(p);
506
+ };
507
+ const addFiles = (list2) => {
508
+ error = "";
509
+ for (const f of list2) {
510
+ const name = f.name || "screenshot.png";
511
+ if (picked.length >= CHAT_FILES_PER_MESSAGE) {
512
+ error = `Up to ${CHAT_FILES_PER_MESSAGE} files per message.`;
513
+ break;
514
+ }
515
+ if (!CHAT_FILE_TYPES.includes(f.type)) {
516
+ error = "Only pictures (PNG, JPEG, WebP, GIF) and PDFs can be sent.";
517
+ continue;
518
+ }
519
+ if (f.size > CHAT_FILE_MAX_BYTES) {
520
+ error = `${name} is larger than 10 MB.`;
521
+ continue;
522
+ }
523
+ const p = { key: `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`, name, type: f.type, size: f.size, local: URL.createObjectURL(f), up: Promise.resolve(), ctrl: new AbortController() };
524
+ p.up = api().uploadChatFile(f, p.ctrl.signal).then(
525
+ (r) => {
526
+ p.id = r.id;
527
+ draw();
528
+ },
529
+ (e) => {
530
+ if (e.name === "AbortError") return;
531
+ dropFile(p);
532
+ error = `${p.name}: ${e.message}`;
533
+ draw();
534
+ }
535
+ );
536
+ picked.push(p);
537
+ }
538
+ if (open && view !== "messages") go("messages");
539
+ else draw();
540
+ text.focus();
541
+ };
542
+ let traySig = "";
543
+ const drawTray = () => {
544
+ const sig = picked.map((p) => `${p.key}:${p.id ? 1 : 0}`).join(",");
545
+ if (sig !== traySig) {
546
+ traySig = sig;
547
+ tray.replaceChildren(
548
+ ...picked.map((p) => {
549
+ const x = el("button", { type: "button", class: "ckw-msgr-file-x", "aria-label": `Remove ${p.name}` }, icon(I.cross, 12));
550
+ x.addEventListener("click", () => {
551
+ dropFile(p);
552
+ draw();
553
+ text.focus();
554
+ });
555
+ const face = p.type.startsWith("image/") ? el("img", { src: p.local, alt: "" }) : el("span", { class: "ckw-msgr-file-doc" }, icon(I.doc, 18), el("span", {}, p.name));
556
+ return el("div", { class: "ckw-msgr-file", role: "listitem", title: p.name, "aria-label": p.id ? p.name : `${p.name}, uploading`, ...p.id ? {} : { "aria-busy": "true" } }, face, x);
557
+ })
558
+ );
559
+ }
560
+ if (!picked.length) tray.remove();
561
+ else if (!tray.isConnected) form.insertBefore(tray, row);
562
+ };
563
+ const shoot = async () => {
564
+ let stream;
565
+ try {
566
+ stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "browser" }, audio: false, preferCurrentTab: true, selfBrowserSurface: "include" });
567
+ } catch (e) {
568
+ if (e.name !== "NotAllowedError" && e.name !== "AbortError") {
569
+ error = "The screenshot did not work here. Attach a picture instead.";
570
+ draw();
571
+ }
572
+ return;
573
+ }
574
+ root.style.visibility = "hidden";
575
+ let blob = null;
576
+ try {
577
+ const video = document.createElement("video");
578
+ video.muted = true;
579
+ video.playsInline = true;
580
+ video.srcObject = stream;
581
+ await video.play();
582
+ await new Promise((r) => setTimeout(r, 300));
583
+ const v = video;
584
+ await new Promise((r) => v.requestVideoFrameCallback ? v.requestVideoFrameCallback(() => r()) : requestAnimationFrame(() => r()));
585
+ const canvas = document.createElement("canvas");
586
+ canvas.width = video.videoWidth;
587
+ canvas.height = video.videoHeight;
588
+ canvas.getContext("2d")?.drawImage(video, 0, 0);
589
+ const encode = (type, q) => new Promise((r) => canvas.toBlob(r, type, q));
590
+ blob = await encode("image/png");
591
+ if (blob && blob.size > 4.5 * 1024 * 1024) blob = await encode("image/jpeg", 0.9);
592
+ } catch {
593
+ blob = null;
594
+ } finally {
595
+ for (const t of stream.getTracks()) t.stop();
596
+ root.style.visibility = "";
597
+ }
598
+ if (!blob || !blob.size) {
599
+ error = "The screenshot did not work here. Attach a picture instead.";
600
+ return draw();
601
+ }
602
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-");
603
+ addFiles([Object.assign(blob, { name: `screenshot-${stamp}.${blob.type === "image/png" ? "png" : "jpg"}` })]);
604
+ };
605
+ const amenu = el("div", { class: "ckw-msgr-menu ckw-msgr-amenu", role: "menu" });
606
+ const amenuOpen = () => amenu.isConnected && !amenu.hasAttribute("data-out");
607
+ const closeAmenu = () => {
608
+ clip.setAttribute("aria-expanded", "false");
609
+ leave(amenu, 120);
610
+ };
611
+ const amenuItem = (d, name, act) => {
612
+ const b = el("button", { type: "button", class: "ckw-msgr-menu-i", role: "menuitem" }, icon(d, 15), el("span", {}, name));
613
+ b.addEventListener("click", (e) => {
614
+ e.stopPropagation();
615
+ closeAmenu();
616
+ act();
617
+ });
618
+ return b;
619
+ };
620
+ clip.addEventListener("click", (e) => {
621
+ e.stopPropagation();
622
+ chime.unlock();
623
+ if (!shots) return picker.click();
624
+ if (amenuOpen()) return closeAmenu();
625
+ amenu.replaceChildren(
626
+ amenuItem(I.image, "Photo or file", () => picker.click()),
627
+ amenuItem(I.screen, "Screenshot of this page", () => void shoot())
628
+ );
629
+ clip.setAttribute("aria-expanded", "true");
630
+ enter(amenu, form);
631
+ amenu.querySelector("button")?.focus();
632
+ });
633
+ picker.addEventListener("change", () => {
634
+ addFiles([...picker.files ?? []]);
635
+ picker.value = "";
636
+ });
637
+ const brand = el("a", { class: "ckw-msgr-powered", href: BRAND_URL, target: "_blank", rel: "noopener" }, "Powered by ", el("b", {}, "MRRocket"));
454
638
  const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": "Messages" });
455
639
  const needEmail = () => askEmail && !o.user()?.email && !thread?.hasEmail;
456
640
  const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());
457
- const canSend = () => !!text.value.trim() && !pending && emailOk();
641
+ const canSend = () => (!!text.value.trim() || picked.length > 0) && !pending && emailOk();
458
642
  const go = (v) => {
459
643
  const changed = JSON.stringify(v) !== JSON.stringify(view);
460
644
  view = v;
@@ -468,9 +652,26 @@ var build = (o) => {
468
652
  } else if (v === "help") helpInput.focus();
469
653
  schedule(0);
470
654
  };
655
+ const picture = (src, alt) => {
656
+ const img = el("img", { src, alt, loading: "lazy" });
657
+ img.addEventListener("load", () => {
658
+ if (body.scrollHeight - body.scrollTop - body.clientHeight < img.clientHeight + 120) body.scrollTop = body.scrollHeight;
659
+ });
660
+ return img;
661
+ };
471
662
  const bubble = (m, isPending = false) => {
472
- const li = el("li", { class: "ckw-msg", "data-from": m.from === "visitor" ? "me" : "them" }, el("div", { class: "ckw-bubble" }, m.body));
663
+ const li = el("li", { class: "ckw-msg", "data-from": m.from === "visitor" ? "me" : "them" }, m.body || !m.attachments?.length ? el("div", { class: "ckw-bubble" }, m.body) : null);
473
664
  if (isPending) li.setAttribute("aria-busy", "true");
665
+ if (m.attachments?.length)
666
+ li.append(
667
+ el(
668
+ "div",
669
+ { class: "ckw-msg-files" },
670
+ ...m.attachments.map(
671
+ (f) => f.type.startsWith("image/") ? el("a", { class: "ckw-msg-img", href: fileUrl(f.id), target: "_blank", rel: "noopener", title: f.name }, picture(fileUrl(f.id), f.name)) : el("a", { class: "ckw-msgr-cite", href: fileUrl(f.id), target: "_blank", rel: "noopener" }, icon(I.doc, 14), el("span", {}, f.name))
672
+ )
673
+ )
674
+ );
474
675
  for (const a of m.articles ?? []) {
475
676
  const chip = el("button", { type: "button", class: "ckw-msgr-cite" }, icon(I.doc, 14), el("span", {}, a.title));
476
677
  chip.addEventListener("click", () => go({ article: a.id }));
@@ -535,29 +736,43 @@ var build = (o) => {
535
736
  }
536
737
  draw();
537
738
  };
739
+ const askForm = el("form", { class: "ckw-msgr-askq" });
740
+ const askIn = el("input", { class: "ckw-msgr-askq-in", placeholder: "Ask a question", "aria-label": "Ask a question", maxlength: "4000", autocomplete: "off" });
741
+ const askGo = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": "Send", disabled: "" }, icon(I.up));
742
+ askForm.append(askIn, askGo);
743
+ askIn.addEventListener("input", () => askGo.disabled = !askIn.value.trim());
744
+ askForm.addEventListener("submit", (e) => {
745
+ e.preventDefault();
746
+ if (!askIn.value.trim()) return go("messages");
747
+ text.value = askIn.value;
748
+ askIn.value = "";
749
+ askGo.disabled = true;
750
+ go("messages");
751
+ if (canSend()) void submit();
752
+ });
538
753
  const drawHome = () => {
754
+ const first = o.user()?.name?.trim().split(/\s+/)[0];
755
+ const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
539
756
  const hero = el(
540
- "div",
541
- { class: "ckw-msgr-hero" },
542
- el("span", { class: "ckw-msgr-brand" }, avatar("sm"), el("span", {}, title)),
543
- el("p", { class: "ckw-msgr-hello" }, greeting),
544
- el("span", { class: "ckw-msgr-online" }, reply)
757
+ "h2",
758
+ { class: "ckw-msgr-hero ckw-msgr-hello" },
759
+ el("span", {}, first ? `Hi, ${first} ` : "Hi there ", el("span", { class: "ckw-msgr-wave", "aria-hidden": "true" }, "\u{1F44B}")),
760
+ el("span", { class: "ckw-msgr-hello-sub" }, rest)
545
761
  );
546
762
  const last = (thread?.messages ?? []).at(-1);
547
- const parts = [hero];
763
+ const parts = [hero, askForm];
548
764
  if (last) {
549
765
  const recent = el(
550
766
  "button",
551
- { type: "button", class: "ckw-msgr-card ckw-msgr-recent" },
552
- el("span", { class: "ckw-msgr-card-label" }, "Recent message"),
767
+ { type: "button", class: "ckw-msgr-card ckw-msgr-recent", "aria-label": `Recent message: ${lineOf(last)}` },
553
768
  el(
554
769
  "span",
555
770
  { class: "ckw-msgr-recent-row" },
556
- avatar(),
771
+ avatar("sm"),
557
772
  el(
558
773
  "span",
559
774
  { class: "ckw-msgr-card-main" },
560
- el("span", { class: "ckw-msgr-card-sub ckw-msgr-recent-text" }, last.body),
775
+ el("span", { class: "ckw-msgr-card-sub ckw-msgr-recent-text" }, lineOf(last)),
561
776
  el("span", { class: "ckw-msgr-card-meta" }, `${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`)
562
777
  ),
563
778
  unseen.length ? el("span", { class: "ckw-msgr-badge" }, String(unseen.length)) : icon(I.next, 14)
@@ -566,17 +781,9 @@ var build = (o) => {
566
781
  recent.addEventListener("click", () => go("messages"));
567
782
  parts.push(recent);
568
783
  }
569
- const msgCard = el(
570
- "button",
571
- { type: "button", class: "ckw-msgr-card" },
572
- el("span", { class: "ckw-msgr-card-main" }, el("b", {}, "Send us a message"), el("span", { class: "ckw-msgr-card-sub" }, reply)),
573
- el("span", { class: "ckw-msgr-go" }, icon(I.send))
574
- );
575
- msgCard.addEventListener("click", () => go("messages"));
576
- parts.push(msgCard);
577
784
  if (helpOn && hasArticles !== false) {
578
- const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, searchBox());
579
- const top = (articles ?? []).slice(0, 4);
785
+ const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" });
786
+ const top = (articles ?? []).slice(0, 3);
580
787
  if (top.length) helpCard.append(...top.map(articleRow));
581
788
  else if (articles === null) helpCard.append(el("div", { class: "ckw-skel ckw-msgr-skel" }));
582
789
  parts.push(helpCard);
@@ -667,20 +874,13 @@ var build = (o) => {
667
874
  let drawn = -1;
668
875
  let listSig = "";
669
876
  let listCount = -1;
670
- const intro = () => el(
671
- "div",
672
- { class: "ckw-msgr-intro" },
673
- el("span", { class: "ckw-msgr-intro-av" }, avatar()),
674
- el("span", { class: "ckw-msgr-intro-t" }, el("b", {}, title), el("span", { class: "ckw-msgr-online" }, reply))
675
- );
676
- const introNode = intro();
677
877
  const suggestions = el("div", { class: "ckw-msgr-sugg" });
678
878
  const drawMessages = () => {
679
879
  const msgs = thread?.messages ?? [];
680
- const parts = [introNode];
880
+ const parts = [];
681
881
  if (!loaded) parts.push(el("div", { class: "ckw-skel" }));
682
882
  else {
683
- const sig = `${msgs.map((m) => m.id).join(",")}|${pending ? pending.body : ""}|${Math.floor(Date.now() / 6e4)}`;
883
+ const sig = `${msgs.map((m) => m.id).join(",")}|${pending ? `${pending.body}+${pending.attachments?.length ?? 0}` : ""}|${Math.floor(Date.now() / 6e4)}`;
684
884
  if (sig !== listSig) {
685
885
  listSig = sig;
686
886
  const greet = el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting));
@@ -716,8 +916,9 @@ var build = (o) => {
716
916
  if (needEmail()) {
717
917
  if (emailOk()) email.removeAttribute("aria-invalid");
718
918
  else email.setAttribute("aria-invalid", "true");
719
- if (!email.isConnected) form.insertBefore(email, row);
919
+ if (!email.isConnected) form.insertBefore(email, tray.isConnected ? tray : row);
720
920
  } else email.remove();
921
+ drawTray();
721
922
  send.disabled = !canSend();
722
923
  };
723
924
  const draw = () => {
@@ -728,11 +929,11 @@ var build = (o) => {
728
929
  else if (view === "messages") drawMessages();
729
930
  else if (view === "help") drawHelp();
730
931
  else drawArticle(view.article);
731
- const kids = [bar, body];
732
- if (view === "messages") kids.push(form);
932
+ const kids = view === "home" ? [sky, bar, body] : [bar, body];
933
+ kids.push(view === "messages" ? form : tabs);
934
+ if (o.branding !== false) kids.push(brand);
733
935
  if (menu.isConnected) kids.push(menu);
734
936
  if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);
735
- placeMark();
736
937
  };
737
938
  const tick = async () => {
738
939
  ctrl?.abort();
@@ -764,20 +965,46 @@ var build = (o) => {
764
965
  document.addEventListener("visibilitychange", onVisible);
765
966
  const submit = async () => {
766
967
  const msg = text.value.trim();
767
- if (!msg || pending || !emailOk()) return;
968
+ const files = picked;
969
+ if (!msg && !files.length || pending || !emailOk()) return;
768
970
  chime.unlock();
769
- pending = { id: "pending", from: "visitor", body: msg, at: (/* @__PURE__ */ new Date()).toISOString() };
971
+ for (const p of files) localUrls.set(p.key, p.local);
972
+ pending = {
973
+ id: "pending",
974
+ from: "visitor",
975
+ body: msg,
976
+ at: (/* @__PURE__ */ new Date()).toISOString(),
977
+ ...files.length ? { attachments: files.map((p) => ({ id: p.key, name: p.name, type: p.type, size: p.size })) } : {}
978
+ };
770
979
  text.value = "";
980
+ picked = [];
771
981
  error = "";
772
982
  draw();
983
+ await Promise.all(files.map((p) => p.up));
984
+ const ids = files.filter((p) => p.id).map((p) => p.id);
985
+ let sent = false;
773
986
  try {
774
- take(await api().sendChat({ body: msg, email: needEmail() && email.value.trim() ? email.value.trim() : void 0, url: window.location.href.slice(0, 500) }));
987
+ if (!msg && !ids.length) throw new Error(error || "The file could not be sent.");
988
+ take(
989
+ await api().sendChat({
990
+ body: msg,
991
+ email: needEmail() && email.value.trim() ? email.value.trim() : void 0,
992
+ url: window.location.href.slice(0, 500),
993
+ ...ids.length ? { attachments: ids } : {},
994
+ // A little context for a bug report; the server reads browser and system from the user agent.
995
+ context: { viewport: `${window.innerWidth}\xD7${window.innerHeight}` }
996
+ })
997
+ );
998
+ sent = true;
775
999
  } catch (e) {
776
1000
  text.value = msg;
1001
+ picked = files.filter((p) => p.id);
777
1002
  error = e.message;
778
1003
  }
779
1004
  pending = null;
1005
+ for (const p of files) localUrls.delete(p.key);
780
1006
  draw();
1007
+ if (sent) for (const p of files) URL.revokeObjectURL(p.local);
781
1008
  schedule();
782
1009
  };
783
1010
  form.addEventListener("submit", (e) => {
@@ -791,6 +1018,27 @@ var build = (o) => {
791
1018
  }
792
1019
  });
793
1020
  text.addEventListener("input", () => send.disabled = !canSend());
1021
+ text.addEventListener("paste", (e) => {
1022
+ const fs = [...e.clipboardData?.files ?? []];
1023
+ if (!fs.length) return;
1024
+ e.preventDefault();
1025
+ addFiles(fs);
1026
+ });
1027
+ const hasFiles = (e) => !!e.dataTransfer && [...e.dataTransfer.types].includes("Files");
1028
+ pop.addEventListener("dragover", (e) => {
1029
+ if (!hasFiles(e)) return;
1030
+ e.preventDefault();
1031
+ pop.setAttribute("data-drop", "");
1032
+ });
1033
+ pop.addEventListener("dragleave", (e) => {
1034
+ if (!e.relatedTarget || !pop.contains(e.relatedTarget)) pop.removeAttribute("data-drop");
1035
+ });
1036
+ pop.addEventListener("drop", (e) => {
1037
+ if (!hasFiles(e)) return;
1038
+ e.preventDefault();
1039
+ pop.removeAttribute("data-drop");
1040
+ addFiles([...e.dataTransfer?.files ?? []]);
1041
+ });
794
1042
  email.addEventListener("input", () => draw());
795
1043
  function show(v = "home", still = false) {
796
1044
  const was = open;
@@ -820,9 +1068,13 @@ var build = (o) => {
820
1068
  schedule();
821
1069
  if (launcherOn) fab.focus();
822
1070
  }
1071
+ pop.addEventListener("click", () => amenuOpen() && closeAmenu());
823
1072
  const onKey = (e) => {
824
1073
  if (e.key !== "Escape" || !open) return;
825
- if (menuOpen()) {
1074
+ if (amenuOpen()) {
1075
+ closeAmenu();
1076
+ clip.focus();
1077
+ } else if (menuOpen()) {
826
1078
  closeMenu();
827
1079
  menuBtn.focus();
828
1080
  } else close();
@@ -841,6 +1093,8 @@ var build = (o) => {
841
1093
  clearTimeout(teaserTimer);
842
1094
  for (const t of leaving.values()) clearTimeout(t);
843
1095
  ctrl?.abort();
1096
+ for (const p of picked) forget(p);
1097
+ for (const u of localUrls.values()) URL.revokeObjectURL(u);
844
1098
  document.removeEventListener("keydown", onKey);
845
1099
  document.removeEventListener("visibilitychange", onVisible);
846
1100
  root.remove();