@mrrocket/sdk 0.13.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/README.md +43 -3
  3. package/dist/api-client.d.ts +62 -1
  4. package/dist/api-client.js +28 -1
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +11 -2
  7. package/dist/browser/mrrocket-messenger.js +304 -70
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +1 -1
  10. package/dist/browser.js +2 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/client.d.ts +12 -0
  13. package/dist/client.js +22 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/context.d.ts +3 -0
  16. package/dist/context.js.map +1 -1
  17. package/dist/core.d.ts +4 -1
  18. package/dist/core.js +1 -1
  19. package/dist/core.js.map +1 -1
  20. package/dist/flows-ui.d.ts +6 -4
  21. package/dist/flows-ui.js +147 -5
  22. package/dist/flows-ui.js.map +1 -1
  23. package/dist/flows.d.ts +51 -1
  24. package/dist/flows.js +62 -21
  25. package/dist/flows.js.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/messenger-ui.d.ts +37 -2
  30. package/dist/messenger-ui.js +597 -109
  31. package/dist/messenger-ui.js.map +1 -1
  32. package/dist/messenger.d.ts +11 -3
  33. package/dist/messenger.js +6 -3
  34. package/dist/messenger.js.map +1 -1
  35. package/dist/paywall.d.ts +123 -0
  36. package/dist/paywall.js +131 -0
  37. package/dist/paywall.js.map +1 -0
  38. package/dist/react-native/index.d.cts +235 -8
  39. package/dist/react-native/index.d.ts +235 -8
  40. package/dist/react-native/index.js +650 -184
  41. package/dist/react-native/index.js.map +1 -1
  42. package/dist/styles-css.d.ts +1 -1
  43. package/dist/styles-css.js +302 -68
  44. package/dist/styles-css.js.map +1 -1
  45. package/package.json +5 -1
@@ -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;
@@ -40,7 +48,7 @@ var el = (tag, attrs = {}, ...kids) => {
40
48
  var SVG_NS = "http://www.w3.org/2000/svg";
41
49
  var icon = (d, size = 16) => {
42
50
  const svg = document.createElementNS(SVG_NS, "svg");
43
- for (const [k, v] of Object.entries({ class: "ckw-icon", width: String(size), height: String(size), viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", "stroke-width": "1.7", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }))
51
+ for (const [k, v] of Object.entries({ class: "ckw-icon", width: String(size), height: String(size), viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.75", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }))
44
52
  svg.setAttribute(k, v);
45
53
  const path = document.createElementNS(SVG_NS, "path");
46
54
  path.setAttribute("d", d);
@@ -48,17 +56,50 @@ var icon = (d, size = 16) => {
48
56
  return svg;
49
57
  };
50
58
  var I = {
51
- cross: "m4 4 8 8M12 4l-8 8",
52
- chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
53
- send: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5",
54
- up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
55
- home: "M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z",
56
- help: "M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1",
57
- back: "M10 3.5 5.5 8l4.5 4.5",
58
- next: "M6 3.5 10.5 8 6 12.5",
59
- search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
60
- doc: "M4 1.5h5.5l3 3V14a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5zM9.5 1.5v3h3M6 8.5h4M6 11h4"
59
+ cross: "M18 6 6 18M6 6l12 12",
60
+ chat: "M7.9 20A9 9 0 1 0 4 16.1L2 22z",
61
+ send: "M5 12h14M12 5l7 7-7 7",
62
+ up: "M12 19V5M5 12l7-7 7 7",
63
+ home: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8M3 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",
64
+ back: "m15 18-6-6 6-6",
65
+ next: "m9 18 6-6-6-6",
66
+ search: "M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0zM21 21l-4.3-4.3",
67
+ doc: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8",
68
+ book: "M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",
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",
70
+ expand: "M15 3h6v6M21 3l-7 7M3 21l7-7M9 21H3v-6",
71
+ collapse: "m14 10 7-7M20 10h-6V4M3 21l7-7M4 14h6v6",
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"
61
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";
81
+ var BUBBLE = "M7.9 20A9 9 0 1 0 4 16.1L2 22z";
82
+ var filled = (d, size) => {
83
+ const svg = document.createElementNS(SVG_NS, "svg");
84
+ for (const [k, v] of Object.entries({ class: "ckw-icon", width: String(size), height: String(size), viewBox: "0 0 24 24", fill: "currentColor", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round", "aria-hidden": "true" }))
85
+ svg.setAttribute(k, v);
86
+ const path = document.createElementNS(SVG_NS, "path");
87
+ path.setAttribute("d", d);
88
+ svg.appendChild(path);
89
+ return svg;
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";
62
103
  var chime = /* @__PURE__ */ (() => {
63
104
  let ctx = null;
64
105
  const get = () => {
@@ -135,11 +176,11 @@ var mountMessenger = (o) => {
135
176
  const start = (look) => {
136
177
  if (real || gone) return;
137
178
  const defined = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
138
- 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 });
139
180
  for (const fn of queued.splice(0)) fn(real);
140
181
  };
141
182
  const timer = setTimeout(() => start(null), LOOK_WAIT_MS);
142
- createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() }).messengerLook(ctrl.signal).then((look) => start(look)).catch(() => start(null)).finally(() => clearTimeout(timer));
183
+ (o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() })).messengerLook(ctrl.signal).then((look) => start(look)).catch(() => start(null)).finally(() => clearTimeout(timer));
143
184
  return {
144
185
  open: (v) => later((h) => h.open(v)),
145
186
  close: () => real?.close(),
@@ -152,10 +193,11 @@ var mountMessenger = (o) => {
152
193
  }
153
194
  };
154
195
  };
196
+ var PLACES = ["top-left", "top-center", "top-right", "middle-left", "middle-right", "bottom-left", "bottom-center", "bottom-right"];
155
197
  var build = (o) => {
156
198
  injectStyles();
157
- const position = o.position ?? "bottom-right";
158
- const title = o.title || "Chat with us";
199
+ const position = PLACES.find((p) => p === o.position) ?? "bottom-right";
200
+ const title = o.title || "Support";
159
201
  const greeting = o.greeting || "Hi! How can we help?";
160
202
  const askEmail = o.askEmail ?? true;
161
203
  const label = o.label ?? "";
@@ -165,13 +207,21 @@ var build = (o) => {
165
207
  const helpOn = o.help ?? true;
166
208
  const soundOn = o.sound ?? true;
167
209
  const previewOn = o.preview ?? true;
168
- const api = () => createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() });
169
- const root = el("div", { class: "ckw-root", "data-theme": o.theme ?? "auto" });
210
+ const api = () => o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() });
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
+ });
218
+ if (o.container) root.setAttribute("data-contained", "");
170
219
  if (o.accent) {
171
220
  root.style.setProperty("--ckw-accent", o.accent);
172
221
  root.style.setProperty("--ckw-accent-ink", inkOn(o.accent));
173
222
  }
174
- document.body.appendChild(root);
223
+ o.glow?.slice(0, 2).forEach((c, i) => /^#[0-9a-f]{6}$/i.test(c) && root.style.setProperty(`--ckw-glow${i + 2}`, c));
224
+ (o.container ?? document.body).appendChild(root);
175
225
  let open = false;
176
226
  let view = "home";
177
227
  let thread = null;
@@ -187,6 +237,8 @@ var build = (o) => {
187
237
  let query = "";
188
238
  let rated = /* @__PURE__ */ new Set();
189
239
  let hasArticles = null;
240
+ let collection = null;
241
+ let wide = false;
190
242
  const teamMsgs = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
191
243
  const lastTeamId = (t) => teamMsgs(t).at(-1)?.id ?? "";
192
244
  const watching = () => open && view === "messages" && document.visibilityState !== "hidden";
@@ -217,24 +269,65 @@ var build = (o) => {
217
269
  if (id) writeSeen(id);
218
270
  };
219
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" });
279
+ const leaving = /* @__PURE__ */ new Map();
280
+ const enter = (node, parent = root) => {
281
+ clearTimeout(leaving.get(node));
282
+ leaving.delete(node);
283
+ node.removeAttribute("data-out");
284
+ if (!node.isConnected) parent.appendChild(node);
285
+ };
286
+ const leave = (node, ms = 180) => {
287
+ if (!node.isConnected || leaving.has(node)) return;
288
+ node.setAttribute("data-out", "");
289
+ leaving.set(
290
+ node,
291
+ setTimeout(() => {
292
+ leaving.delete(node);
293
+ node.removeAttribute("data-out");
294
+ node.remove();
295
+ }, ms)
296
+ );
297
+ };
298
+ const replay = (node, cls) => {
299
+ node.classList.remove(cls);
300
+ void node.offsetWidth;
301
+ node.classList.add(cls);
302
+ };
220
303
  const fab = el("button", { type: "button", class: `ckw-fab ckw-msgr-fab${label ? "" : " ckw-msgr-round"}`, "data-pos": position, "aria-haspopup": "dialog" });
304
+ const fabIcons = el(
305
+ "span",
306
+ { class: "ckw-msgr-fab-ic", "aria-hidden": "true" },
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))
309
+ );
310
+ const fabCount = el("span", { class: "ckw-msgr-count", "aria-hidden": "true" });
311
+ fab.append(fabIcons);
312
+ if (label) fab.append(el("span", {}, label));
221
313
  const prev = el("div", { class: "ckw-msgr-preview", "data-pos": position, role: "status" });
222
- let fabShows = null;
223
314
  const drawFab = () => {
224
- const glyph = icon(open ? I.cross : I.chat, label ? 18 : 24);
225
- if (fabShows !== null && fabShows !== open) glyph.classList.add("ckw-turn");
226
- fabShows = open;
227
- fab.replaceChildren(glyph);
228
- if (label) fab.append(el("span", {}, label));
229
- if (unseen.length && !open) fab.append(el("span", { class: "ckw-msgr-count", "aria-hidden": "true" }, String(Math.min(unseen.length, 9))));
315
+ fab.toggleAttribute("data-open", open);
316
+ if (unseen.length && !open) {
317
+ const n = String(Math.min(unseen.length, 9));
318
+ if (fabCount.textContent !== n) fabCount.textContent = n;
319
+ if (!fabCount.isConnected) fab.append(fabCount);
320
+ } else fabCount.remove();
230
321
  fab.setAttribute("aria-expanded", String(open));
231
322
  fab.setAttribute("aria-label", open ? "Close chat" : unseen.length ? `Open chat, ${unseen.length} new ${unseen.length === 1 ? "reply" : "replies"}` : "Open chat");
232
323
  drawPreview();
233
324
  };
325
+ const previewShown = () => previewOn && !open && !previewGone && !!unseen.length && launcherOn;
234
326
  const drawPreview = () => {
235
327
  const m = unseen.at(-1);
236
- if (!previewOn || open || previewGone || !m || !launcherOn) return prev.remove();
237
- const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 12));
328
+ if (!previewShown() || !m) return leave(prev);
329
+ hideTeaser(true);
330
+ const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 14));
238
331
  x.addEventListener("click", (e) => {
239
332
  e.stopPropagation();
240
333
  previewGone = true;
@@ -243,50 +336,315 @@ var build = (o) => {
243
336
  const card = el(
244
337
  "button",
245
338
  { type: "button", class: "ckw-msgr-preview-card" },
246
- avatar(),
247
339
  el(
248
340
  "span",
249
341
  { class: "ckw-msgr-preview-main" },
250
342
  el("span", { class: "ckw-msgr-preview-who" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` \xB7 ${unseen.length} new` : ""}`),
251
- el("span", { class: "ckw-msgr-preview-text" }, m.body)
343
+ el("span", { class: "ckw-msgr-preview-text" }, lineOf(m))
252
344
  )
253
345
  );
254
346
  card.addEventListener("click", () => show("messages"));
255
347
  prev.replaceChildren(card, x);
256
- if (!prev.isConnected) root.appendChild(prev);
348
+ enter(prev);
257
349
  };
350
+ const teaser = el("div", { class: "ckw-msgr-teaser", "data-pos": position });
351
+ let teaserTimer;
352
+ const showTeaser = () => {
353
+ clearTimeout(teaserTimer);
354
+ if (open || previewShown() || !launcherOn) return;
355
+ const chat = el("button", { type: "button", class: "ckw-msgr-teaser-b", "data-main": "" }, icon(I.chat), el("span", {}, "Chat"));
356
+ chat.addEventListener("click", () => show("messages"));
357
+ const buttons = [chat];
358
+ if (helpOn && hasArticles) {
359
+ const search = el("button", { type: "button", class: "ckw-msgr-teaser-b" }, icon(I.search), el("span", {}, "Search"));
360
+ search.addEventListener("click", () => show("help"));
361
+ buttons.push(search);
362
+ }
363
+ teaser.replaceChildren(
364
+ el(
365
+ "div",
366
+ { class: "ckw-msgr-teaser-top" },
367
+ el("span", { class: "ckw-msgr-teaser-t" }, el("b", {}, greeting), el("span", { class: "ckw-msgr-online" }, reply)),
368
+ avatar()
369
+ ),
370
+ el("div", { class: "ckw-msgr-teaser-row" }, ...buttons)
371
+ );
372
+ enter(teaser);
373
+ };
374
+ function hideTeaser(now = false) {
375
+ clearTimeout(teaserTimer);
376
+ if (now) return leave(teaser, 140);
377
+ teaserTimer = setTimeout(() => leave(teaser, 140), 260);
378
+ }
379
+ const hoverable = (e) => e.pointerType === "mouse";
380
+ for (const node of [fab, teaser]) {
381
+ node.addEventListener("pointerenter", (e) => hoverable(e) && (node === fab ? showTeaser() : clearTimeout(teaserTimer)));
382
+ node.addEventListener("pointerleave", (e) => hoverable(e) && hideTeaser());
383
+ }
258
384
  fab.addEventListener("click", () => {
259
385
  chime.unlock();
386
+ hideTeaser(true);
260
387
  if (open) close();
261
388
  else show(unseen.length ? "messages" : "home");
262
389
  });
263
390
  if (launcherOn) root.appendChild(fab);
264
391
  const pop = el("div", { class: "ckw-pop ckw-msgr", "data-pos": position, role: "dialog", "aria-label": title });
265
- const head = el("div", { class: "ckw-pop-head ckw-msgr-head" });
392
+ const bar = el("div", { class: "ckw-msgr-bar" });
266
393
  const body = el("div", { class: "ckw-pop-body ckw-msgr-body" });
267
- const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": "Messenger" });
268
394
  pop.addEventListener("click", () => chime.unlock());
395
+ const lead = el("span", { class: "ckw-msgr-lead" });
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));
400
+ b.addEventListener("click", () => {
401
+ collection = null;
402
+ go(v);
403
+ });
404
+ return b;
405
+ };
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);
409
+ const tabCount = el("span", { class: "ckw-msgr-count ckw-msgr-count-tab", "aria-hidden": "true" });
410
+ const menuBtn = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": "More", "aria-haspopup": "menu", "aria-expanded": "false" }, icon(I.dots, 18));
411
+ const xBtn = el("button", { type: "button", class: "ckw-msgr-ib ckw-msgr-x", "aria-label": "Close messenger" }, icon(I.cross));
412
+ xBtn.addEventListener("click", () => close());
413
+ bar.append(lead, mid, el("span", { class: "ckw-msgr-trail" }, menuBtn, xBtn));
414
+ const menu = el("div", { class: "ckw-msgr-menu", role: "menu" });
415
+ const menuOpen = () => menu.isConnected && !menu.hasAttribute("data-out");
416
+ const closeMenu = () => {
417
+ menuBtn.setAttribute("aria-expanded", "false");
418
+ leave(menu, 120);
419
+ };
420
+ const menuItem = (d, name, act) => {
421
+ const b = el("button", { type: "button", class: "ckw-msgr-menu-i", role: "menuitem" }, icon(d, 15), el("span", {}, name));
422
+ b.addEventListener("click", (e) => {
423
+ e.stopPropagation();
424
+ closeMenu();
425
+ act();
426
+ });
427
+ return b;
428
+ };
429
+ menuBtn.addEventListener("click", (e) => {
430
+ e.stopPropagation();
431
+ if (menuOpen()) return closeMenu();
432
+ menu.replaceChildren(
433
+ el("span", { class: "ckw-msgr-menu-h" }, "Quick actions"),
434
+ menuItem(I.minus, "Minimize chat", () => close()),
435
+ menuItem(wide ? I.collapse : I.expand, wide ? "Collapse view" : "Expand view", () => {
436
+ wide = !wide;
437
+ pop.toggleAttribute("data-wide", wide);
438
+ })
439
+ );
440
+ menuBtn.setAttribute("aria-expanded", "true");
441
+ enter(menu, pop);
442
+ menu.querySelector("button")?.focus();
443
+ });
444
+ pop.addEventListener("click", () => menuOpen() && closeMenu());
445
+ const drawBar = () => {
446
+ const back = (to, name) => {
447
+ const b = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": name }, icon(I.back, 18));
448
+ b.addEventListener("click", to);
449
+ return b;
450
+ };
451
+ if (typeof view !== "string")
452
+ lead.replaceChildren(
453
+ back(() => go("help"), "Back to help")
454
+ );
455
+ else if (view === "help" && collection && !query.trim())
456
+ lead.replaceChildren(
457
+ back(() => {
458
+ collection = null;
459
+ go("help");
460
+ }, "All collections")
461
+ );
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");
471
+ chatTab.setAttribute("aria-current", view === "messages" ? "page" : "false");
472
+ helpTab.setAttribute("aria-current", view !== "home" && view !== "messages" ? "page" : "false");
473
+ if (unseen.length && view !== "messages") {
474
+ tabCount.textContent = String(Math.min(unseen.length, 9));
475
+ if (!tabCount.isConnected) chatTab.firstChild.appendChild(tabCount);
476
+ } else tabCount.remove();
477
+ };
269
478
  const form = el("form", { class: "ckw-msgr-compose" });
270
479
  const err = el("p", { class: "ckw-msgr-error", role: "alert" });
271
480
  const email = el("input", { class: "ckw-input", type: "email", inputmode: "email", autocomplete: "email", placeholder: "Your email (optional), so we can follow up", "aria-label": "Your email (optional)", maxlength: "200" });
272
- const row = el("div", { class: "ckw-msgr-row" });
273
- const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write a message\u2026", "aria-label": "Message", maxlength: "4000" });
481
+ const row = el("div", { class: "ckw-msgr-box" });
482
+ const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write your message\u2026", "aria-label": "Message", maxlength: "4000" });
274
483
  const send = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": "Send" }, icon(I.up));
275
- row.append(text, 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));
491
+ row.addEventListener("click", (e) => e.target === row && text.focus());
276
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"));
277
638
  const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": "Messages" });
278
639
  const needEmail = () => askEmail && !o.user()?.email && !thread?.hasEmail;
279
640
  const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());
280
- const canSend = () => !!text.value.trim() && !pending && emailOk();
281
- const closeBtn = () => {
282
- const b = el("button", { type: "button", class: "ckw-btn ckw-btn-ghost ckw-msgr-x", "aria-label": "Close messenger" }, icon(I.cross));
283
- b.addEventListener("click", () => close());
284
- return b;
285
- };
641
+ const canSend = () => (!!text.value.trim() || picked.length > 0) && !pending && emailOk();
286
642
  const go = (v) => {
643
+ const changed = JSON.stringify(v) !== JSON.stringify(view);
287
644
  view = v;
288
645
  drawn = -1;
289
646
  draw();
647
+ if (changed) replay(body, "ckw-msgr-enter");
290
648
  if (v === "messages") {
291
649
  markSeen();
292
650
  drawFab();
@@ -294,9 +652,26 @@ var build = (o) => {
294
652
  } else if (v === "help") helpInput.focus();
295
653
  schedule(0);
296
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
+ };
297
662
  const bubble = (m, isPending = false) => {
298
- 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);
299
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
+ );
300
675
  for (const a of m.articles ?? []) {
301
676
  const chip = el("button", { type: "button", class: "ckw-msgr-cite" }, icon(I.doc, 14), el("span", {}, a.title));
302
677
  chip.addEventListener("click", () => go({ article: a.id }));
@@ -310,9 +685,19 @@ var build = (o) => {
310
685
  b.addEventListener("click", () => go({ article: a.id }));
311
686
  return b;
312
687
  };
313
- const searchBox = () => {
688
+ const helpRow = (name, sub, meta, act) => {
689
+ const b = el(
690
+ "button",
691
+ { type: "button", class: "ckw-msgr-hrow" },
692
+ el("span", { class: "ckw-msgr-hrow-main" }, el("b", {}, name), sub ? el("span", { class: "ckw-msgr-hrow-sub" }, sub) : null, meta ? el("span", { class: "ckw-msgr-hrow-meta" }, meta) : null),
693
+ icon(I.next, 16)
694
+ );
695
+ b.addEventListener("click", act);
696
+ return b;
697
+ };
698
+ const searchBox = (placeholder = "Search for help") => {
314
699
  const wrap = el("label", { class: "ckw-msgr-search" });
315
- const input = el("input", { class: "ckw-msgr-search-in", type: "search", placeholder: "Search for help", "aria-label": "Search for help", maxlength: "200" });
700
+ const input = el("input", { class: "ckw-msgr-search-in", type: "search", placeholder, "aria-label": "Search for help", maxlength: "200" });
316
701
  input.value = query;
317
702
  let t;
318
703
  input.addEventListener("input", () => {
@@ -329,10 +714,10 @@ var build = (o) => {
329
714
  if (view !== "help") go("help");
330
715
  }
331
716
  });
332
- wrap.append(input, icon(I.search, 16));
717
+ wrap.append(icon(I.search, 16), input);
333
718
  return wrap;
334
719
  };
335
- const helpSearch = searchBox();
720
+ const helpSearch = searchBox("Search our help center\u2026");
336
721
  const helpInput = helpSearch.querySelector("input");
337
722
  let loadingArticles = false;
338
723
  const loadArticles = async (force = false) => {
@@ -351,24 +736,43 @@ var build = (o) => {
351
736
  }
352
737
  draw();
353
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
+ });
354
753
  const drawHome = () => {
355
- head.replaceChildren(el("span", { class: "ckw-msgr-brand" }, avatar("sm"), el("span", {}, title)), closeBtn());
356
- const hero = el("div", { class: "ckw-msgr-hero" }, el("p", { class: "ckw-msgr-hello" }, greeting));
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?";
756
+ const hero = el(
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)
761
+ );
357
762
  const last = (thread?.messages ?? []).at(-1);
358
- const parts = [hero];
763
+ const parts = [hero, askForm];
359
764
  if (last) {
360
765
  const recent = el(
361
766
  "button",
362
- { type: "button", class: "ckw-msgr-card ckw-msgr-recent" },
363
- 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)}` },
364
768
  el(
365
769
  "span",
366
770
  { class: "ckw-msgr-recent-row" },
367
- avatar(),
771
+ avatar("sm"),
368
772
  el(
369
773
  "span",
370
774
  { class: "ckw-msgr-card-main" },
371
- 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)),
372
776
  el("span", { class: "ckw-msgr-card-meta" }, `${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`)
373
777
  ),
374
778
  unseen.length ? el("span", { class: "ckw-msgr-badge" }, String(unseen.length)) : icon(I.next, 14)
@@ -377,17 +781,9 @@ var build = (o) => {
377
781
  recent.addEventListener("click", () => go("messages"));
378
782
  parts.push(recent);
379
783
  }
380
- const msgCard = el(
381
- "button",
382
- { type: "button", class: "ckw-msgr-card" },
383
- el("span", { class: "ckw-msgr-card-main" }, el("b", {}, "Send us a message"), el("span", { class: "ckw-msgr-card-sub" }, reply)),
384
- el("span", { class: "ckw-msgr-go" }, icon(I.send))
385
- );
386
- msgCard.addEventListener("click", () => go("messages"));
387
- parts.push(msgCard);
388
784
  if (helpOn && hasArticles !== false) {
389
- const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, searchBox());
390
- 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);
391
787
  if (top.length) helpCard.append(...top.map(articleRow));
392
788
  else if (articles === null) helpCard.append(el("div", { class: "ckw-skel ckw-msgr-skel" }));
393
789
  parts.push(helpCard);
@@ -395,30 +791,50 @@ var build = (o) => {
395
791
  body.replaceChildren(...parts);
396
792
  };
397
793
  const drawHelp = () => {
398
- head.replaceChildren(el("span", {}, "Help"), closeBtn());
399
794
  if (helpInput.value !== query) helpInput.value = query;
400
- const parts = [helpSearch];
795
+ const q = query.trim();
796
+ const all = articles ?? [];
797
+ const groups = /* @__PURE__ */ new Map();
798
+ for (const a of all) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
799
+ const inside = !q && collection !== null ? groups.get(collection) ?? [] : null;
800
+ const heading = q ? "Search results" : inside ? collection ?? "" : "Most read articles";
801
+ const parts = [el("h2", { class: "ckw-msgr-help-h" }, heading), helpSearch];
401
802
  if (articles === null) parts.push(el("div", { class: "ckw-skel" }));
402
- else if (!articles.length) {
803
+ else if (!all.length) {
403
804
  const ask = el("button", { type: "button", class: "ckw-btn ckw-btn-primary" }, "Send us a message");
404
805
  ask.addEventListener("click", () => go("messages"));
405
- parts.push(el("div", { class: "ckw-msgr-empty" }, el("p", {}, query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), ask));
406
- } else if (query.trim()) parts.push(el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, ...articles.map(articleRow)));
806
+ parts.push(el("div", { class: "ckw-msgr-empty" }, el("p", {}, q ? `Nothing found for \u201C${q}\u201D.` : "No help articles yet."), ask));
807
+ } else if (q || inside)
808
+ parts.push(
809
+ el(
810
+ "div",
811
+ { class: "ckw-msgr-hlist" },
812
+ ...(inside ?? all).map((a) => helpRow(a.title, a.excerpt.startsWith(a.title) ? a.excerpt.slice(a.title.length).trim() : a.excerpt, q ? a.collection : "", () => go({ article: a.id })))
813
+ )
814
+ );
815
+ else
816
+ parts.push(
817
+ el(
818
+ "div",
819
+ { class: "ckw-msgr-hlist" },
820
+ ...[...groups].map(
821
+ ([name, items]) => helpRow(name, items.map((a) => a.title).join(", "), `${items.length} ${items.length === 1 ? "article" : "articles"}`, () => {
822
+ collection = name;
823
+ go("help");
824
+ replay(body, "ckw-msgr-enter");
825
+ })
826
+ )
827
+ )
828
+ );
829
+ if (body.childNodes[1] !== helpSearch) body.replaceChildren(...parts);
407
830
  else {
408
- const groups = /* @__PURE__ */ new Map();
409
- for (const a of articles) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
410
- for (const [name, items] of groups)
411
- parts.push(el("section", { class: "ckw-msgr-group" }, el("h3", { class: "ckw-msgr-group-h" }, `${name} \xB7 ${items.length}`), el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, ...items.map(articleRow))));
831
+ body.firstChild.replaceWith(parts[0]);
832
+ while (body.childNodes.length > 2) body.lastChild.remove();
833
+ body.append(...parts.slice(2));
412
834
  }
413
- if (body.firstChild !== helpSearch) body.replaceChildren(helpSearch);
414
- while (body.childNodes.length > 1) body.lastChild.remove();
415
- body.append(...parts.slice(1));
416
835
  };
417
836
  let articleShown = null;
418
837
  const drawArticle = (id) => {
419
- const back = el("button", { type: "button", class: "ckw-btn ckw-btn-ghost ckw-msgr-x", "aria-label": "Back to help" }, icon(I.back));
420
- back.addEventListener("click", () => go("help"));
421
- head.replaceChildren(back, el("span", { class: "ckw-msgr-head-t" }, "Help"), closeBtn());
422
838
  if (articleShown?.id === id) return body.replaceChildren(articleShown.node);
423
839
  const node = el("article", { class: "ckw-msgr-doc" }, el("div", { class: "ckw-skel" }));
424
840
  articleShown = { id, node };
@@ -457,22 +873,35 @@ var build = (o) => {
457
873
  };
458
874
  let drawn = -1;
459
875
  let listSig = "";
876
+ let listCount = -1;
877
+ const suggestions = el("div", { class: "ckw-msgr-sugg" });
460
878
  const drawMessages = () => {
461
- head.replaceChildren(
462
- el("span", { class: "ckw-msgr-who" }, avatar(), el("span", { class: "ckw-msgr-who-t" }, el("b", {}, title), el("span", {}, reply))),
463
- closeBtn()
464
- );
465
879
  const msgs = thread?.messages ?? [];
466
880
  const parts = [];
467
881
  if (!loaded) parts.push(el("div", { class: "ckw-skel" }));
468
882
  else {
469
- 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)}`;
470
884
  if (sig !== listSig) {
471
885
  listSig = sig;
472
886
  const greet = el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting));
473
- list.replaceChildren(greet, ...msgs.map((m) => bubble(m)), ...pending ? [bubble(pending, true)] : []);
887
+ const items = [greet, ...msgs.map((m) => bubble(m)), ...pending ? [bubble(pending, true)] : []];
888
+ if (listCount >= 0) items.slice(listCount).forEach((li) => li.setAttribute("data-new", ""));
889
+ listCount = items.length;
890
+ list.replaceChildren(...items);
474
891
  }
475
892
  parts.push(list);
893
+ const top = !msgs.some((m) => m.from === "visitor") && !pending ? (articles ?? []).slice(0, 3) : [];
894
+ if (top.length) {
895
+ if (suggestions.childNodes.length !== top.length)
896
+ suggestions.replaceChildren(
897
+ ...top.map((a) => {
898
+ const b = el("button", { type: "button", class: "ckw-msgr-sugg-b" }, a.title);
899
+ b.addEventListener("click", () => go({ article: a.id }));
900
+ return b;
901
+ })
902
+ );
903
+ parts.push(suggestions);
904
+ }
476
905
  }
477
906
  if (thread?.conversation?.status === "solved" && !pending) parts.push(el("p", { class: "ckw-fine ckw-msgr-note" }, "This conversation is closed. Write to start a new one."));
478
907
  if (body.childNodes.length !== parts.length || parts.some((x, i) => body.childNodes[i] !== x)) body.replaceChildren(...parts);
@@ -487,31 +916,23 @@ var build = (o) => {
487
916
  if (needEmail()) {
488
917
  if (emailOk()) email.removeAttribute("aria-invalid");
489
918
  else email.setAttribute("aria-invalid", "true");
490
- if (!email.isConnected) form.insertBefore(email, row);
919
+ if (!email.isConnected) form.insertBefore(email, tray.isConnected ? tray : row);
491
920
  } else email.remove();
921
+ drawTray();
492
922
  send.disabled = !canSend();
493
923
  };
494
- const drawTabs = () => {
495
- const current = typeof view === "string" ? view : "help";
496
- const tab = (v, name, d, badge = 0) => {
497
- const b = el("button", { type: "button", class: "ckw-msgr-tab", "aria-current": current === v ? "page" : "false" }, icon(d, 18), el("span", {}, name));
498
- if (badge) b.append(el("span", { class: "ckw-msgr-count ckw-msgr-count-tab" }, String(Math.min(badge, 9))));
499
- b.addEventListener("click", () => go(v));
500
- return b;
501
- };
502
- tabs.replaceChildren(tab("home", "Home", I.home), tab("messages", "Messages", I.chat, view === "messages" ? 0 : unseen.length), ...helpOn && hasArticles !== false ? [tab("help", "Help", I.help)] : []);
503
- };
504
924
  const draw = () => {
505
925
  if (!open) return;
506
926
  pop.dataset.view = typeof view === "string" ? view : "article";
927
+ drawBar();
507
928
  if (view === "home") drawHome();
508
929
  else if (view === "messages") drawMessages();
509
930
  else if (view === "help") drawHelp();
510
931
  else drawArticle(view.article);
511
- const kids = [head, body];
512
- if (view === "messages") kids.push(form);
513
- kids.push(tabs);
514
- drawTabs();
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);
935
+ if (menu.isConnected) kids.push(menu);
515
936
  if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);
516
937
  };
517
938
  const tick = async () => {
@@ -537,26 +958,53 @@ var build = (o) => {
537
958
  const ms = now ?? (watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : open || waiting ? POLL_IDLE_MS : 0);
538
959
  if (ms || now === 0) poll = setTimeout(() => void tick(), ms);
539
960
  }
540
- document.addEventListener("visibilitychange", () => {
961
+ const onVisible = () => {
541
962
  if (document.visibilityState === "visible") schedule(0);
542
963
  else clearTimeout(poll);
543
- });
964
+ };
965
+ document.addEventListener("visibilitychange", onVisible);
544
966
  const submit = async () => {
545
967
  const msg = text.value.trim();
546
- if (!msg || pending || !emailOk()) return;
968
+ const files = picked;
969
+ if (!msg && !files.length || pending || !emailOk()) return;
547
970
  chime.unlock();
548
- 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
+ };
549
979
  text.value = "";
980
+ picked = [];
550
981
  error = "";
551
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;
552
986
  try {
553
- 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;
554
999
  } catch (e) {
555
1000
  text.value = msg;
1001
+ picked = files.filter((p) => p.id);
556
1002
  error = e.message;
557
1003
  }
558
1004
  pending = null;
1005
+ for (const p of files) localUrls.delete(p.key);
559
1006
  draw();
1007
+ if (sent) for (const p of files) URL.revokeObjectURL(p.local);
560
1008
  schedule();
561
1009
  };
562
1010
  form.addEventListener("submit", (e) => {
@@ -570,13 +1018,38 @@ var build = (o) => {
570
1018
  }
571
1019
  });
572
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
+ });
573
1042
  email.addEventListener("input", () => draw());
574
- function show(v = "home") {
1043
+ function show(v = "home", still = false) {
1044
+ const was = open;
575
1045
  open = true;
576
1046
  view = v === "help" && !helpOn ? "home" : v;
577
1047
  drawn = -1;
578
1048
  previewGone = true;
579
- if (!pop.isConnected) root.appendChild(pop);
1049
+ hideTeaser(true);
1050
+ pop.toggleAttribute("data-still", still);
1051
+ enter(pop);
1052
+ if (was) replay(body, "ckw-msgr-enter");
580
1053
  if (articles === null || view === "help" && query) void loadArticles();
581
1054
  draw();
582
1055
  drawFab();
@@ -589,26 +1062,41 @@ var build = (o) => {
589
1062
  function close() {
590
1063
  if (!open) return;
591
1064
  open = false;
592
- pop.remove();
1065
+ if (menu.isConnected) closeMenu();
1066
+ leave(pop, 200);
593
1067
  drawFab();
594
1068
  schedule();
595
1069
  if (launcherOn) fab.focus();
596
1070
  }
1071
+ pop.addEventListener("click", () => amenuOpen() && closeAmenu());
597
1072
  const onKey = (e) => {
598
- if (e.key === "Escape" && open) close();
1073
+ if (e.key !== "Escape" || !open) return;
1074
+ if (amenuOpen()) {
1075
+ closeAmenu();
1076
+ clip.focus();
1077
+ } else if (menuOpen()) {
1078
+ closeMenu();
1079
+ menuBtn.focus();
1080
+ } else close();
599
1081
  };
600
1082
  document.addEventListener("keydown", onKey);
601
1083
  void tick();
602
1084
  if (helpOn) void loadArticles();
603
1085
  drawFab();
1086
+ if (o.opened) show(o.opened, true);
604
1087
  return {
605
1088
  open: (v) => show(v ?? "home"),
606
1089
  close,
607
1090
  toggle: () => open ? close() : show(),
608
1091
  destroy: () => {
609
1092
  clearTimeout(poll);
1093
+ clearTimeout(teaserTimer);
1094
+ for (const t of leaving.values()) clearTimeout(t);
610
1095
  ctrl?.abort();
1096
+ for (const p of picked) forget(p);
1097
+ for (const u of localUrls.values()) URL.revokeObjectURL(u);
611
1098
  document.removeEventListener("keydown", onKey);
1099
+ document.removeEventListener("visibilitychange", onVisible);
612
1100
  root.remove();
613
1101
  }
614
1102
  };