@mrrocket/sdk 0.17.0 → 0.21.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 (69) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +24 -1
  3. package/dist/api-client.d.ts +28 -4
  4. package/dist/api-client.js +30 -13
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +15 -3
  7. package/dist/browser/mrrocket-messenger.js +38 -5
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +2 -2
  10. package/dist/browser.js +2 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/client.d.ts +28 -3
  13. package/dist/client.js +37 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/context.d.ts +3 -1
  16. package/dist/context.js.map +1 -1
  17. package/dist/core.d.ts +7 -1
  18. package/dist/core.js +1 -1
  19. package/dist/core.js.map +1 -1
  20. package/dist/feedback.js +161 -119
  21. package/dist/feedback.js.map +1 -1
  22. package/dist/finish-web.d.ts +18 -0
  23. package/dist/finish-web.js +139 -0
  24. package/dist/finish-web.js.map +1 -0
  25. package/dist/flows-ui.d.ts +1 -10
  26. package/dist/flows-ui.js +175 -48
  27. package/dist/flows-ui.js.map +1 -1
  28. package/dist/flows-web.d.ts +8 -1
  29. package/dist/flows-web.js +10 -1
  30. package/dist/flows-web.js.map +1 -1
  31. package/dist/flows.d.ts +193 -9
  32. package/dist/flows.js +284 -38
  33. package/dist/flows.js.map +1 -1
  34. package/dist/i18n.d.ts +22 -0
  35. package/dist/i18n.js +74 -0
  36. package/dist/i18n.js.map +1 -0
  37. package/dist/icons.d.ts +24 -0
  38. package/dist/icons.js +61 -0
  39. package/dist/icons.js.map +1 -0
  40. package/dist/index.d.ts +6 -2
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/messenger-ui.d.ts +9 -3
  44. package/dist/messenger-ui.js +388 -191
  45. package/dist/messenger-ui.js.map +1 -1
  46. package/dist/messenger.d.ts +3 -1
  47. package/dist/messenger.js +3 -2
  48. package/dist/messenger.js.map +1 -1
  49. package/dist/onboarding-state.d.ts +23 -0
  50. package/dist/onboarding-state.js +43 -0
  51. package/dist/onboarding-state.js.map +1 -0
  52. package/dist/paywall.js +2 -1
  53. package/dist/paywall.js.map +1 -1
  54. package/dist/react-native/index.d.cts +239 -19
  55. package/dist/react-native/index.d.ts +239 -19
  56. package/dist/react-native/index.js +1305 -518
  57. package/dist/react-native/index.js.map +1 -1
  58. package/dist/server.d.ts +4 -2
  59. package/dist/server.js +2 -2
  60. package/dist/server.js.map +1 -1
  61. package/dist/styles-css.d.ts +1 -1
  62. package/dist/styles-css.js +35 -2
  63. package/dist/styles-css.js.map +1 -1
  64. package/dist/survey.js +18 -14
  65. package/dist/survey.js.map +1 -1
  66. package/dist/ui.d.ts +2 -0
  67. package/dist/ui.js +15 -12
  68. package/dist/ui.js.map +1 -1
  69. package/package.json +6 -3
@@ -8,35 +8,40 @@ import {
8
8
  inkOn,
9
9
  replyLine
10
10
  } from "./api-client.js";
11
+ import { currentLocale, onWords, t, tf, wordsLang, wordsVersion } from "./i18n.js";
11
12
  import { injectStyles } from "./styles-css.js";
13
+ var UPDATES_SHOWN = 20;
12
14
  var POLL_OPEN_MS = 4e3;
13
15
  var POLL_WAITING_MS = 8e3;
14
16
  var POLL_IDLE_MS = 3e4;
17
+ var POLL_QUIET_MS = 6e4;
15
18
  var WAITING_FOR_MS = 10 * 6e4;
19
+ var SLOW_SEND_MS = 1500;
16
20
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
17
21
  var SEEN_KEY = "ckw_msgr_seen";
22
+ var SEEN_CHATS_KEY = "ckw_msgr_seen_chats";
18
23
  var readSeen = () => {
19
24
  try {
20
- return window.localStorage.getItem(SEEN_KEY) ?? "";
25
+ return { old: window.localStorage.getItem(SEEN_KEY) ?? "", chats: JSON.parse(window.localStorage.getItem(SEEN_CHATS_KEY) ?? "{}") };
21
26
  } catch {
22
- return "";
27
+ return { old: "", chats: {} };
23
28
  }
24
29
  };
25
- var writeSeen = (id) => {
30
+ var writeSeen = (chats) => {
26
31
  try {
27
- window.localStorage.setItem(SEEN_KEY, id);
32
+ window.localStorage.setItem(SEEN_CHATS_KEY, JSON.stringify(chats));
28
33
  } catch {
29
34
  }
30
35
  };
31
36
  var timeAgo = (iso) => {
32
- const t = new Date(iso).getTime();
33
- if (Number.isNaN(t)) return "";
34
- const d = Math.max(0, Date.now() - t) / 1e3;
35
- if (d < 60) return "just now";
36
- if (d < 3600) return `${Math.floor(d / 60)}m ago`;
37
- if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
38
- if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;
39
- return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
37
+ const at = new Date(iso).getTime();
38
+ if (Number.isNaN(at)) return "";
39
+ const d = Math.max(0, Date.now() - at) / 1e3;
40
+ if (d < 60) return t("just now");
41
+ if (d < 3600) return tf("{n}m ago", { n: Math.floor(d / 60) });
42
+ if (d < 86400) return tf("{n}h ago", { n: Math.floor(d / 3600) });
43
+ if (d < 86400 * 30) return tf("{n}d ago", { n: Math.floor(d / 86400) });
44
+ return new Date(iso).toLocaleDateString(currentLocale(), { month: "short", day: "numeric", year: "numeric" });
40
45
  };
41
46
  var el = (tag, attrs = {}, ...kids) => {
42
47
  const e = document.createElement(tag);
@@ -71,11 +76,12 @@ var I = {
71
76
  minus: "M5 12h14",
72
77
  down: "m6 9 6 6 6-6",
73
78
  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",
79
+ updates: "m3 11 18-5v12L3 14zM11.6 16.8a3 3 0 1 1-5.8-1.6",
74
80
  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",
75
81
  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",
76
82
  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"
77
83
  };
78
- var lineOf = (m) => m.body || (m.attachments?.length === 1 ? `Sent ${m.attachments[0].name}` : m.attachments?.length ? `Sent ${m.attachments.length} files` : "");
84
+ var lineOf = (m) => m.body || (m.attachments?.length === 1 ? tf("Sent {name}", { name: m.attachments[0].name }) : m.attachments?.length ? tf("Sent {n} files", { n: m.attachments.length }) : "");
79
85
  var canShoot = () => typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
80
86
  var BUBBLE = "M7.9 20A9 9 0 1 0 4 16.1L2 22z";
81
87
  var filled = (d, size) => {
@@ -99,6 +105,7 @@ 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 1
99
105
  var SMILE = "M9.5 15.5c1.4 1 3.6 1 5 0";
100
106
  var DISC = "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z";
101
107
  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";
108
+ var MEGAPHONE = "m3 11 18-5v12L3 14zM5.8 15.2a3 3 0 0 0 5.8 1.6z";
102
109
  var chime = /* @__PURE__ */ (() => {
103
110
  let ctx = null;
104
111
  const get = () => {
@@ -120,11 +127,11 @@ var chime = /* @__PURE__ */ (() => {
120
127
  try {
121
128
  const c = get();
122
129
  if (!c || c.state !== "running") return;
123
- const t = c.currentTime;
130
+ const t2 = c.currentTime;
124
131
  [880, 1318.5].forEach((f, i) => {
125
132
  const o = c.createOscillator();
126
133
  const g = c.createGain();
127
- const at = t + i * 0.11;
134
+ const at = t2 + i * 0.11;
128
135
  o.type = "sine";
129
136
  o.frequency.value = f;
130
137
  g.gain.setValueAtTime(1e-4, at);
@@ -189,9 +196,11 @@ var mountMessenger = (o) => {
189
196
  const later = (fn) => real ? fn(real) : queued.push(fn);
190
197
  const ctrl = new AbortController();
191
198
  const caching = !o.client && !!o.apiKey;
199
+ let lastLook = null;
192
200
  const start = (look) => {
193
201
  if (gone) return;
194
- const key = JSON.stringify(look ?? {});
202
+ lastLook = look;
203
+ const key = `${wordsVersion()}:${JSON.stringify(look ?? {})}`;
195
204
  if (real && key === shown) return;
196
205
  if (real?.isOpen()) {
197
206
  real.onClose(() => start(look));
@@ -206,18 +215,27 @@ var mountMessenger = (o) => {
206
215
  const cached = caching ? readCachedLook(o.apiKey) : null;
207
216
  if (cached) start(cached);
208
217
  const timer = cached ? void 0 : setTimeout(() => start(null), LOOK_WAIT_MS);
209
- (o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user(), userHash: o.userHash?.() })).messengerLook(ctrl.signal).then((look) => {
218
+ const readLook = () => (o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user(), userHash: o.userHash?.() })).messengerLook(ctrl.signal).then((look) => {
210
219
  if (caching && look) writeCachedLook(o.apiKey, look);
211
220
  start(look);
212
221
  }).catch(() => {
213
- if (!real) start(null);
222
+ start(lastLook);
214
223
  }).finally(() => clearTimeout(timer));
224
+ let readIn = currentLocale();
225
+ void readLook();
226
+ const offWords = onWords(() => {
227
+ if (currentLocale() !== readIn) {
228
+ readIn = currentLocale();
229
+ void readLook();
230
+ } else if (real) start(lastLook);
231
+ });
215
232
  return {
216
233
  open: (v) => later((h) => h.open(v)),
217
234
  close: () => real?.close(),
218
235
  toggle: () => later((h) => h.toggle()),
219
236
  destroy: () => {
220
237
  gone = true;
238
+ offWords();
221
239
  ctrl.abort();
222
240
  clearTimeout(timer);
223
241
  real?.destroy();
@@ -228,15 +246,16 @@ var PLACES = ["top-left", "top-center", "top-right", "middle-left", "middle-righ
228
246
  var build = (o) => {
229
247
  injectStyles();
230
248
  const position = PLACES.find((p) => p === o.position) ?? "bottom-right";
231
- const title = o.title || "Support";
232
- const greeting = o.greeting || "Hi! How can we help?";
249
+ const title = o.title || t("Support");
250
+ const greeting = o.greeting || t("Hi! How can we help?");
233
251
  const askEmail = o.askEmail ?? true;
234
252
  const label = o.label ?? "";
235
253
  const reply = replyLine(o.replyTime);
236
254
  const initial = (title.trim()[0] ?? "?").toUpperCase();
237
- const whoLabel = { visitor: "You", team: "Team", ai: o.agentName?.trim() || "AI agent", auto: "Team" };
255
+ const whoLabel = { visitor: t("You"), team: t("Team"), ai: o.agentName?.trim() || t("AI agent"), auto: t("Team") };
238
256
  const launcherOn = o.launcher ?? true;
239
257
  const helpOn = o.help ?? true;
258
+ const updatesOn = o.updates ?? true;
240
259
  const soundOn = o.sound ?? true;
241
260
  const previewOn = o.preview ?? true;
242
261
  const api = () => o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user(), userHash: o.userHash?.() });
@@ -245,7 +264,9 @@ var build = (o) => {
245
264
  "data-theme": o.theme ?? "auto",
246
265
  "data-corners": o.corners === "soft" || o.corners === "square" ? o.corners : "round",
247
266
  "data-font": o.font === "system" ? "system" : "site",
248
- "data-header": o.header === "color" ? "color" : "plain"
267
+ "data-header": o.header === "color" ? "color" : "plain",
268
+ // Screen readers read the panel in the language of its words. Built anew when they change.
269
+ ...wordsLang() ? { lang: wordsLang() } : {}
249
270
  });
250
271
  if (o.container) root.setAttribute("data-contained", "");
251
272
  if (o.accent) {
@@ -258,49 +279,96 @@ var build = (o) => {
258
279
  let afterClose = null;
259
280
  let view = "home";
260
281
  let thread = null;
282
+ let chats = [];
283
+ let hasEmail = false;
284
+ let chatId = null;
285
+ let draft = null;
286
+ let drafts = 0;
287
+ let listing = false;
261
288
  let loaded = false;
262
289
  let error = "";
263
- let pending = null;
264
- let lastKnown = null;
290
+ const sayable = (e) => e.status === 401 ? t("The chat is not available right now. Please try again later.") : e.message;
291
+ let outbox = [];
292
+ let outboxSeq = 0;
293
+ let flushing = false;
294
+ const known = /* @__PURE__ */ new Map();
295
+ let seen = readSeen();
265
296
  let unseen = [];
266
297
  let previewGone = false;
267
298
  let poll;
299
+ let refused = false;
268
300
  let ctrl = null;
269
301
  let articles = null;
270
302
  let query = "";
271
303
  let rated = /* @__PURE__ */ new Set();
272
304
  let hasArticles = null;
305
+ let updates = null;
306
+ let updatesNode = null;
273
307
  let collection = null;
274
308
  let wide = false;
275
- const teamMsgs = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
276
- const lastTeamId = (t) => teamMsgs(t).at(-1)?.id ?? "";
309
+ const isAnswer = (m) => m.from === "team" || m.from === "ai";
277
310
  const watching = () => open && view === "messages" && document.visibilityState !== "hidden";
278
- const take = (t) => {
279
- const team = teamMsgs(t);
280
- if (lastKnown === null) {
281
- const seen = readSeen();
282
- const i = team.findIndex((m) => m.id === seen);
283
- unseen = t.messages.some((m) => m.from === "visitor") ? team.slice(i + 1).filter((m) => m.from !== "auto") : [];
311
+ const shownChat = () => listing || draft !== null ? null : chatId ?? thread?.conversation?.id ?? null;
312
+ const unseenIn = (chat) => unseen.filter((m) => m.chat === chat).length;
313
+ const learn = (chat, answers, newest, settled) => {
314
+ const was = known.get(chat);
315
+ const after = (at) => answers.filter((m) => m.at > at);
316
+ if (was === void 0) {
317
+ const read = seen.chats[chat];
318
+ const fresh = settled ? [] : read ? after(read) : answers.slice(answers.findIndex((m) => m.id === seen.old) + 1);
319
+ unseen = [...unseen, ...fresh.map((m) => ({ ...m, chat }))];
320
+ if (loaded && fresh.length) {
321
+ if (soundOn) chime.play();
322
+ previewGone = false;
323
+ }
284
324
  } else {
285
- const i = team.findIndex((m) => m.id === lastKnown);
286
- const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
325
+ const fresh = after(was);
287
326
  if (fresh.length) {
288
327
  if (soundOn) chime.play();
289
- if (!watching()) {
290
- unseen = [...unseen, ...fresh];
328
+ if (!watching() || shownChat() !== chat) {
329
+ unseen = [...unseen, ...fresh.map((m) => ({ ...m, chat }))];
291
330
  previewGone = false;
292
331
  }
293
332
  }
294
333
  }
295
- lastKnown = lastTeamId(t);
296
- thread = t;
334
+ known.set(chat, newest ?? was ?? "");
335
+ };
336
+ const take = (t2) => {
337
+ const read = t2.conversation;
338
+ const last = t2.messages.at(-1);
339
+ chats = t2.conversations ?? (read && last ? [{ id: read.id, status: read.status, subject: lineOf(t2.messages[0]), last }] : []);
340
+ hasEmail = t2.hasEmail;
341
+ chats.forEach((c, i) => {
342
+ if (c.id !== read?.id) learn(c.id, c.last && isAnswer(c.last) ? [c.last] : [], c.last?.at, c.status === "solved" && i > 0);
343
+ });
344
+ if (read) learn(read.id, t2.messages.filter(isAnswer), last?.at, read.status === "solved" && chats[0]?.id !== read.id);
345
+ if (chatId === null || read?.id === chatId) thread = t2;
297
346
  if (watching()) markSeen();
298
347
  };
299
348
  const markSeen = () => {
300
- unseen = [];
301
- const id = lastTeamId(thread);
302
- if (id) writeSeen(id);
349
+ const chat = shownChat();
350
+ if (!chat) return;
351
+ unseen = unseen.filter((m) => m.chat !== chat);
352
+ const at = known.get(chat);
353
+ if (!at || seen.chats[chat] === at) return;
354
+ const kept = Object.fromEntries(Object.entries(seen.chats).filter(([c]) => chats.some((x) => x.id === c)));
355
+ seen = { ...seen, chats: { ...kept, [chat]: at } };
356
+ writeSeen(seen.chats);
303
357
  };
358
+ const enterChat = (id, d, list2 = false) => {
359
+ if (id !== chatId || d !== draft) {
360
+ ctrl?.abort();
361
+ ctrl = null;
362
+ thread = null;
363
+ listSig = "";
364
+ listCount = -1;
365
+ }
366
+ chatId = id;
367
+ draft = d;
368
+ listing = list2;
369
+ };
370
+ const enterMessages = () => enterChat(null, null, chats.length > 1);
371
+ const startChat = () => enterChat(null, ++drafts);
304
372
  const letter = (size) => el("span", { class: `ckw-msgr-av${size ? " ckw-msgr-av-sm" : ""}`, "aria-hidden": "true" }, initial);
305
373
  const avatar = (size) => {
306
374
  if (!o.icon) return letter(size);
@@ -361,14 +429,14 @@ var build = (o) => {
361
429
  if (!fabCount.isConnected) fab.append(fabCount);
362
430
  } else fabCount.remove();
363
431
  fab.setAttribute("aria-expanded", String(open));
364
- fab.setAttribute("aria-label", open ? "Close chat" : unseen.length ? `Open chat, ${unseen.length} new ${unseen.length === 1 ? "reply" : "replies"}` : "Open chat");
432
+ fab.setAttribute("aria-label", open ? t("Close chat") : unseen.length === 1 ? t("Open chat, 1 new reply") : unseen.length ? tf("Open chat, {n} new replies", { n: unseen.length }) : t("Open chat"));
365
433
  drawPreview();
366
434
  };
367
435
  const previewShown = () => previewOn && !open && !previewGone && !!unseen.length && launcherOn;
368
436
  const drawPreview = () => {
369
437
  const m = unseen.at(-1);
370
438
  if (!previewShown() || !m) return leave(prev);
371
- const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 14));
439
+ const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": t("Dismiss") }, icon(I.cross, 14));
372
440
  x.addEventListener("click", (e) => {
373
441
  e.stopPropagation();
374
442
  previewGone = true;
@@ -380,18 +448,23 @@ var build = (o) => {
380
448
  el(
381
449
  "span",
382
450
  { class: "ckw-msgr-preview-main" },
383
- el("span", { class: "ckw-msgr-preview-who" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` \xB7 ${unseen.length} new` : ""}`),
451
+ el("span", { class: "ckw-msgr-preview-who" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` \xB7 ${tf("{n} new", { n: unseen.length })}` : ""}`),
384
452
  el("span", { class: "ckw-msgr-preview-text" }, lineOf(m))
385
453
  )
386
454
  );
387
- card.addEventListener("click", () => show("messages"));
455
+ card.addEventListener("click", () => {
456
+ enterChat(m.chat, null);
457
+ show("messages");
458
+ });
388
459
  prev.replaceChildren(card, x);
389
460
  enter(prev);
390
461
  };
391
462
  fab.addEventListener("click", () => {
392
463
  chime.unlock();
393
- if (open) close();
394
- else show(unseen.length ? "messages" : "home");
464
+ if (open) return close();
465
+ const news = unseen.at(-1);
466
+ if (news) enterChat(news.chat, null);
467
+ show(news ? "messages" : "home");
395
468
  });
396
469
  if (launcherOn) root.appendChild(fab);
397
470
  const pop = el("div", { class: "ckw-pop ckw-msgr", "data-pos": position, role: "dialog", "aria-label": title });
@@ -400,21 +473,23 @@ var build = (o) => {
400
473
  pop.addEventListener("click", () => chime.unlock());
401
474
  const lead = el("span", { class: "ckw-msgr-lead" });
402
475
  const mid = el("span", { class: "ckw-msgr-mid" });
403
- const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": "Messenger" });
476
+ const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": t("Messenger") });
404
477
  const tab = (v, name, line, shape, cut) => {
405
478
  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));
406
479
  b.addEventListener("click", () => {
407
480
  collection = null;
481
+ if (v === "messages") enterMessages();
408
482
  go(v);
409
483
  });
410
484
  return b;
411
485
  };
412
- const homeTab = tab("home", "Home", I.home, HOUSE, SMILE);
413
- const chatTab = tab("messages", "Messages", I.chat, BUBBLE, DOTS);
414
- const helpTab = tab("help", "Help", I.help, DISC, QMARK);
486
+ const homeTab = tab("home", t("Home"), I.home, HOUSE, SMILE);
487
+ const chatTab = tab("messages", t("Messages"), I.chat, BUBBLE, DOTS);
488
+ const helpTab = tab("help", t("Help"), I.help, DISC, QMARK);
489
+ const updatesTab = tab("updates", t("Updates"), I.updates, MEGAPHONE, "");
415
490
  const tabCount = el("span", { class: "ckw-msgr-count ckw-msgr-count-tab", "aria-hidden": "true" });
416
- const menuBtn = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": "More", "aria-haspopup": "menu", "aria-expanded": "false" }, icon(I.dots, 18));
417
- const xBtn = el("button", { type: "button", class: "ckw-msgr-ib ckw-msgr-x", "aria-label": "Close messenger" }, icon(I.cross));
491
+ const menuBtn = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": t("More"), "aria-haspopup": "menu", "aria-expanded": "false" }, icon(I.dots, 18));
492
+ const xBtn = el("button", { type: "button", class: "ckw-msgr-ib ckw-msgr-x", "aria-label": t("Close messenger") }, icon(I.cross));
418
493
  xBtn.addEventListener("click", () => close());
419
494
  bar.append(lead, mid, el("span", { class: "ckw-msgr-trail" }, menuBtn, xBtn));
420
495
  const menu = el("div", { class: "ckw-msgr-menu", role: "menu" });
@@ -436,9 +511,9 @@ var build = (o) => {
436
511
  e.stopPropagation();
437
512
  if (menuOpen()) return closeMenu();
438
513
  menu.replaceChildren(
439
- el("span", { class: "ckw-msgr-menu-h" }, "Quick actions"),
440
- menuItem(I.minus, "Minimize chat", () => close()),
441
- menuItem(wide ? I.collapse : I.expand, wide ? "Collapse view" : "Expand view", () => {
514
+ el("span", { class: "ckw-msgr-menu-h" }, t("Quick actions")),
515
+ menuItem(I.minus, t("Minimize chat"), () => close()),
516
+ menuItem(wide ? I.collapse : I.expand, wide ? t("Collapse view") : t("Expand view"), () => {
442
517
  wide = !wide;
443
518
  pop.toggleAttribute("data-wide", wide);
444
519
  })
@@ -456,26 +531,37 @@ var build = (o) => {
456
531
  };
457
532
  if (typeof view !== "string")
458
533
  lead.replaceChildren(
459
- back(() => go("help"), "Back to help")
534
+ back(() => go("help"), t("Back to help"))
460
535
  );
461
536
  else if (view === "help" && collection && !query.trim())
462
537
  lead.replaceChildren(
463
538
  back(() => {
464
539
  collection = null;
465
540
  go("help");
466
- }, "All collections")
541
+ }, t("All collections"))
467
542
  );
468
- else if (view === "messages") lead.replaceChildren(back(() => go("home"), "Back to home"));
543
+ else if (view === "messages" && !listing && chats.length > 1)
544
+ lead.replaceChildren(
545
+ back(() => {
546
+ enterMessages();
547
+ go("messages");
548
+ }, t("All conversations"))
549
+ );
550
+ else if (view === "messages") lead.replaceChildren(back(() => go("home"), t("Back to home")));
469
551
  else if (view === "home") lead.replaceChildren(logo);
470
552
  else lead.replaceChildren();
471
- if (view === "messages") mid.replaceChildren(avatar("sm"), el("span", { class: "ckw-msgr-mid-t" }, el("b", {}, title), el("span", { class: "ckw-msgr-online" }, reply)));
472
- else if (view === "help") mid.replaceChildren(el("b", {}, "Help"));
553
+ if (view === "messages" && listing) mid.replaceChildren(el("b", {}, t("Messages")));
554
+ else if (view === "messages") mid.replaceChildren(avatar("sm"), el("span", { class: "ckw-msgr-mid-t" }, el("b", {}, title), el("span", { class: "ckw-msgr-online" }, reply)));
555
+ else if (view === "help") mid.replaceChildren(el("b", {}, t("Help")));
556
+ else if (view === "updates") mid.replaceChildren(el("b", {}, t("Updates")));
473
557
  else mid.replaceChildren();
474
- const items = helpOn && hasArticles !== false ? [homeTab, chatTab, helpTab] : [homeTab, chatTab];
475
- if (tabs.childNodes.length !== items.length) tabs.replaceChildren(...items);
558
+ const items = [homeTab, chatTab, ...helpOn && hasArticles !== false ? [helpTab] : [], ...updatesOn && updates?.length ? [updatesTab] : []];
559
+ if (tabs.childNodes.length !== items.length || items.some((t2, i) => tabs.childNodes[i] !== t2)) tabs.replaceChildren(...items);
560
+ tabs.toggleAttribute("data-four", items.length > 3);
476
561
  homeTab.setAttribute("aria-current", view === "home" ? "page" : "false");
477
562
  chatTab.setAttribute("aria-current", view === "messages" ? "page" : "false");
478
- helpTab.setAttribute("aria-current", view !== "home" && view !== "messages" ? "page" : "false");
563
+ helpTab.setAttribute("aria-current", view !== "home" && view !== "messages" && view !== "updates" ? "page" : "false");
564
+ updatesTab.setAttribute("aria-current", view === "updates" ? "page" : "false");
479
565
  if (unseen.length && view !== "messages") {
480
566
  tabCount.textContent = String(Math.min(unseen.length, 9));
481
567
  if (!tabCount.isConnected) chatTab.firstChild.appendChild(tabCount);
@@ -483,14 +569,14 @@ var build = (o) => {
483
569
  };
484
570
  const form = el("form", { class: "ckw-msgr-compose" });
485
571
  const err = el("p", { class: "ckw-msgr-error", role: "alert" });
486
- const email = el("input", { class: "ckw-input ckw-msgr-email", type: "email", inputmode: "email", autocomplete: "email", placeholder: "Email for our reply (optional)", "aria-label": "Your email (optional)", maxlength: "200", "data-1p-ignore": "", "data-lpignore": "true", "data-bwignore": "", "data-form-type": "other" });
572
+ const email = el("input", { class: "ckw-input ckw-msgr-email", type: "email", inputmode: "email", autocomplete: "email", placeholder: t("Email for our reply (optional)"), "aria-label": t("Your email (optional)"), maxlength: "200", "data-1p-ignore": "", "data-lpignore": "true", "data-bwignore": "", "data-form-type": "other" });
487
573
  const row = el("div", { class: "ckw-msgr-box" });
488
- const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write your message\u2026", "aria-label": "Message", maxlength: "4000" });
489
- const send = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": "Send" }, icon(I.up));
574
+ const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: t("Write your message\u2026"), "aria-label": t("Message"), maxlength: "4000" });
575
+ const send = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": t("Send") }, icon(I.up));
490
576
  const shots = canShoot();
491
577
  const clip = el(
492
578
  "button",
493
- { 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" } : {} },
579
+ { type: "button", class: "ckw-msgr-clip", "aria-label": shots ? t("Attach a file or a screenshot") : t("Attach a file"), ...shots ? { "aria-haspopup": "menu", "aria-expanded": "false" } : {} },
494
580
  icon(I.clip, 18)
495
581
  );
496
582
  row.append(text, el("div", { class: "ckw-msgr-tools" }, clip, el("span", { class: "ckw-msgr-sp" }), send));
@@ -498,7 +584,7 @@ var build = (o) => {
498
584
  form.append(row);
499
585
  const picker = el("input", { type: "file", accept: CHAT_FILE_TYPES.join(","), multiple: "", hidden: "", tabindex: "-1", "aria-hidden": "true" });
500
586
  form.append(picker);
501
- const tray = el("div", { class: "ckw-msgr-files", role: "list", "aria-label": "Files to send" });
587
+ const tray = el("div", { class: "ckw-msgr-files", role: "list", "aria-label": t("Files to send") });
502
588
  let picked = [];
503
589
  const localUrls = /* @__PURE__ */ new Map();
504
590
  const fileUrl = (id) => localUrls.get(id) ?? api().attachmentUrl(id);
@@ -515,15 +601,15 @@ var build = (o) => {
515
601
  for (const f of list2) {
516
602
  const name = f.name || "screenshot.png";
517
603
  if (picked.length >= CHAT_FILES_PER_MESSAGE) {
518
- error = `Up to ${CHAT_FILES_PER_MESSAGE} files per message.`;
604
+ error = tf("Up to {n} files per message.", { n: CHAT_FILES_PER_MESSAGE });
519
605
  break;
520
606
  }
521
607
  if (!CHAT_FILE_TYPES.includes(f.type)) {
522
- error = "Only pictures (PNG, JPEG, WebP, GIF) and PDFs can be sent.";
608
+ error = t("Only pictures (PNG, JPEG, WebP, GIF) and PDFs can be sent.");
523
609
  continue;
524
610
  }
525
611
  if (f.size > CHAT_FILE_MAX_BYTES) {
526
- error = `${name} is larger than 10 MB.`;
612
+ error = tf("{name} is larger than 10 MB.", { name });
527
613
  continue;
528
614
  }
529
615
  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() };
@@ -541,8 +627,10 @@ var build = (o) => {
541
627
  );
542
628
  picked.push(p);
543
629
  }
544
- if (open && view !== "messages") go("messages");
545
- else draw();
630
+ if (open && (view !== "messages" || listing)) {
631
+ startChat();
632
+ go("messages");
633
+ } else draw();
546
634
  text.focus();
547
635
  };
548
636
  let traySig = "";
@@ -552,14 +640,14 @@ var build = (o) => {
552
640
  traySig = sig;
553
641
  tray.replaceChildren(
554
642
  ...picked.map((p) => {
555
- const x = el("button", { type: "button", class: "ckw-msgr-file-x", "aria-label": `Remove ${p.name}` }, icon(I.cross, 12));
643
+ const x = el("button", { type: "button", class: "ckw-msgr-file-x", "aria-label": tf("Remove {name}", { name: p.name }) }, icon(I.cross, 12));
556
644
  x.addEventListener("click", () => {
557
645
  dropFile(p);
558
646
  draw();
559
647
  text.focus();
560
648
  });
561
649
  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));
562
- 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);
650
+ return el("div", { class: "ckw-msgr-file", role: "listitem", title: p.name, "aria-label": p.id ? p.name : tf("{name}, uploading", { name: p.name }), ...p.id ? {} : { "aria-busy": "true" } }, face, x);
563
651
  })
564
652
  );
565
653
  }
@@ -572,7 +660,7 @@ var build = (o) => {
572
660
  stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "browser" }, audio: false, preferCurrentTab: true, selfBrowserSurface: "include" });
573
661
  } catch (e) {
574
662
  if (e.name !== "NotAllowedError" && e.name !== "AbortError") {
575
- error = "The screenshot did not work here. Attach a picture instead.";
663
+ error = t("The screenshot did not work here. Attach a picture instead.");
576
664
  draw();
577
665
  }
578
666
  return;
@@ -598,11 +686,11 @@ var build = (o) => {
598
686
  } catch {
599
687
  blob = null;
600
688
  } finally {
601
- for (const t of stream.getTracks()) t.stop();
689
+ for (const t2 of stream.getTracks()) t2.stop();
602
690
  root.style.visibility = "";
603
691
  }
604
692
  if (!blob || !blob.size) {
605
- error = "The screenshot did not work here. Attach a picture instead.";
693
+ error = t("The screenshot did not work here. Attach a picture instead.");
606
694
  return draw();
607
695
  }
608
696
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-");
@@ -629,8 +717,8 @@ var build = (o) => {
629
717
  if (!shots) return picker.click();
630
718
  if (amenuOpen()) return closeAmenu();
631
719
  amenu.replaceChildren(
632
- amenuItem(I.image, "Photo or file", () => picker.click()),
633
- amenuItem(I.screen, "Screenshot of this page", () => void shoot())
720
+ amenuItem(I.image, t("Photo or file"), () => picker.click()),
721
+ amenuItem(I.screen, t("Screenshot of this page"), () => void shoot())
634
722
  );
635
723
  clip.setAttribute("aria-expanded", "true");
636
724
  enter(amenu, form);
@@ -640,11 +728,11 @@ var build = (o) => {
640
728
  addFiles([...picker.files ?? []]);
641
729
  picker.value = "";
642
730
  });
643
- const brand = el("a", { class: "ckw-msgr-powered", href: BRAND_URL, target: "_blank", rel: "noopener" }, "Powered by ", el("b", {}, "MRRocket"));
644
- const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": "Messages" });
645
- const needEmail = () => askEmail && !o.user()?.email && !thread?.hasEmail;
731
+ const brand = el("a", { class: "ckw-msgr-powered", href: BRAND_URL, target: "_blank", rel: "noopener" }, t("Powered by {brand}").split("{brand}")[0], el("b", {}, "MRRocket"), t("Powered by {brand}").split("{brand}")[1]);
732
+ const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": t("Messages") });
733
+ const needEmail = () => askEmail && !o.user()?.email && !hasEmail;
646
734
  const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());
647
- const canSend = () => (!!text.value.trim() || picked.length > 0) && !pending && emailOk();
735
+ const canSend = () => (!!text.value.trim() || picked.length > 0) && emailOk();
648
736
  const go = (v) => {
649
737
  const changed = JSON.stringify(v) !== JSON.stringify(view);
650
738
  view = v;
@@ -666,6 +754,7 @@ var build = (o) => {
666
754
  return img;
667
755
  };
668
756
  const bubble = (m, isPending = false) => {
757
+ const slow = isPending && Date.now() - Date.parse(m.at) >= SLOW_SEND_MS;
669
758
  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);
670
759
  if (isPending) li.setAttribute("aria-busy", "true");
671
760
  if (m.attachments?.length)
@@ -683,7 +772,7 @@ var build = (o) => {
683
772
  chip.addEventListener("click", () => go({ article: a.id }));
684
773
  li.append(chip);
685
774
  }
686
- li.append(el("span", { class: "ckw-msg-meta" }, m.from === "ai" ? agentFace() : null, `${m.from !== "visitor" ? `${whoLabel[m.from]} \xB7 ` : ""}${isPending ? "Sending\u2026" : timeAgo(m.at)}`));
775
+ li.append(el("span", { class: "ckw-msg-meta" }, m.from === "ai" ? agentFace() : null, `${m.from !== "visitor" ? `${whoLabel[m.from]} \xB7 ` : ""}${slow ? t("Sending\u2026") : timeAgo(m.at)}`));
687
776
  return li;
688
777
  };
689
778
  const articleRow = (a) => {
@@ -701,15 +790,15 @@ var build = (o) => {
701
790
  b.addEventListener("click", act);
702
791
  return b;
703
792
  };
704
- const searchBox = (placeholder = "Search for help") => {
793
+ const searchBox = (placeholder = t("Search for help")) => {
705
794
  const wrap = el("label", { class: "ckw-msgr-search" });
706
- const input = el("input", { class: "ckw-msgr-search-in", type: "search", placeholder, "aria-label": "Search for help", maxlength: "200" });
795
+ const input = el("input", { class: "ckw-msgr-search-in", type: "search", placeholder, "aria-label": t("Search for help"), maxlength: "200" });
707
796
  input.value = query;
708
- let t;
797
+ let wait;
709
798
  input.addEventListener("input", () => {
710
799
  query = input.value;
711
- clearTimeout(t);
712
- t = setTimeout(() => {
800
+ clearTimeout(wait);
801
+ wait = setTimeout(() => {
713
802
  if (view !== "help") return go("help");
714
803
  void loadArticles(true);
715
804
  }, 250);
@@ -723,9 +812,13 @@ var build = (o) => {
723
812
  wrap.append(icon(I.search, 16), input);
724
813
  return wrap;
725
814
  };
726
- const helpSearch = searchBox("Search our help center\u2026");
815
+ const helpSearch = searchBox(t("Search our help center\u2026"));
727
816
  const helpInput = helpSearch.querySelector("input");
728
817
  let loadingArticles = false;
818
+ const askAnew = () => {
819
+ startChat();
820
+ go("messages");
821
+ };
729
822
  const loadArticles = async (force = false) => {
730
823
  if (!helpOn || loadingArticles && !force) return;
731
824
  loadingArticles = true;
@@ -742,50 +835,92 @@ var build = (o) => {
742
835
  }
743
836
  draw();
744
837
  };
838
+ const loadUpdates = async () => {
839
+ try {
840
+ updates = (await api().changelog()).entries.slice(0, UPDATES_SHOWN);
841
+ } catch {
842
+ updates = [];
843
+ }
844
+ updatesNode = null;
845
+ draw();
846
+ };
745
847
  const askForm = el("form", { class: "ckw-msgr-askq" });
746
- const askIn = el("input", { class: "ckw-msgr-askq-in", placeholder: "Ask a question", "aria-label": "Ask a question", maxlength: "4000", autocomplete: "off" });
747
- const askGo = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": "Send", disabled: "" }, icon(I.up));
848
+ const askIn = el("input", { class: "ckw-msgr-askq-in", placeholder: t("Ask a question"), "aria-label": t("Ask a question"), maxlength: "4000", autocomplete: "off" });
849
+ const askGo = el("button", { type: "submit", class: "ckw-msgr-send", "aria-label": t("Send"), disabled: "" }, icon(I.up));
748
850
  askForm.append(askIn, askGo);
749
851
  askIn.addEventListener("input", () => askGo.disabled = !askIn.value.trim());
750
852
  askForm.addEventListener("submit", (e) => {
751
853
  e.preventDefault();
752
- if (!askIn.value.trim()) return go("messages");
854
+ if (!askIn.value.trim()) {
855
+ enterMessages();
856
+ return go("messages");
857
+ }
753
858
  text.value = askIn.value;
754
859
  askIn.value = "";
755
860
  askGo.disabled = true;
861
+ startChat();
756
862
  go("messages");
757
- if (canSend()) void submit();
863
+ if (canSend()) submit();
758
864
  });
865
+ const chatRow = (c) => {
866
+ const m = c.last;
867
+ const n = unseenIn(c.id);
868
+ const closed = c.status === "solved" ? ` \xB7 ${t("Closed")}` : "";
869
+ const row2 = el(
870
+ "button",
871
+ { type: "button", class: "ckw-msgr-card ckw-msgr-recent", "aria-label": `${tf("Conversation: {subject}", { subject: c.subject })}${closed}${n ? `, ${tf("{n} new", { n })}` : ""}` },
872
+ el(
873
+ "span",
874
+ { class: "ckw-msgr-recent-row" },
875
+ avatar("sm"),
876
+ el(
877
+ "span",
878
+ { class: "ckw-msgr-card-main" },
879
+ el("span", { class: "ckw-msgr-card-sub ckw-msgr-recent-text" }, c.subject),
880
+ m && lineOf(m) !== c.subject ? el("span", { class: "ckw-msgr-card-sub" }, lineOf(m)) : null,
881
+ m ? el("span", { class: "ckw-msgr-card-meta" }, `${whoLabel[m.from]} \xB7 ${timeAgo(m.at)}${closed}`) : null
882
+ ),
883
+ n ? el("span", { class: "ckw-msgr-badge" }, String(n)) : icon(I.next, 14)
884
+ )
885
+ );
886
+ row2.addEventListener("click", () => {
887
+ enterChat(c.id, null);
888
+ go("messages");
889
+ });
890
+ return row2;
891
+ };
892
+ const chatsBox = el("div", { class: "ckw-msgr-chats" });
893
+ const newChat = el("button", { type: "button", class: "ckw-btn ckw-btn-primary ckw-msgr-new" }, icon(I.chat), t("New conversation"));
894
+ newChat.addEventListener("click", askAnew);
895
+ let chatsSig = "";
896
+ const drawChats = () => {
897
+ const sig = `${chats.map((c) => `${c.id}:${c.status}:${c.last?.id}:${unseenIn(c.id)}`).join()}|${Math.floor(Date.now() / 6e4)}`;
898
+ if (sig !== chatsSig) {
899
+ chatsSig = sig;
900
+ chatsBox.replaceChildren(...chats.map(chatRow));
901
+ }
902
+ if (body.childNodes[0] !== chatsBox) body.replaceChildren(chatsBox, newChat);
903
+ };
759
904
  const drawHome = () => {
760
905
  const first = o.user()?.name?.trim().split(/\s+/)[0];
761
- const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
906
+ const rest = (o.greeting ? greeting.replace(/^(hi|hello|hey|hallo|moin|servus|bonjour|salut|hola|ciao|salve|hoi|ol[áa]|cze[śs][ćc]|hej|こんにちは)( there)?\s*[!,.!、。]*\s*/i, "").trim() : t("How can we help?")) || t("How can we help you today?");
762
907
  const hero = el(
763
908
  "h2",
764
909
  { class: "ckw-msgr-hero ckw-msgr-hello" },
765
- el("span", {}, first ? `Hi, ${first} ` : "Hi there ", el("span", { class: "ckw-msgr-wave", "aria-hidden": "true" }, "\u{1F44B}")),
910
+ el("span", {}, `${first ? tf("Hi, {name}", { name: first }) : t("Hi there")} `, el("span", { class: "ckw-msgr-wave", "aria-hidden": "true" }, "\u{1F44B}")),
766
911
  el("span", { class: "ckw-msgr-hello-sub" }, rest)
767
912
  );
768
- const last = (thread?.messages ?? []).at(-1);
769
913
  const parts = [hero, askForm];
770
- if (last) {
771
- const recent = el(
772
- "button",
773
- { type: "button", class: "ckw-msgr-card ckw-msgr-recent", "aria-label": `Recent message: ${lineOf(last)}` },
774
- el(
775
- "span",
776
- { class: "ckw-msgr-recent-row" },
777
- avatar("sm"),
778
- el(
779
- "span",
780
- { class: "ckw-msgr-card-main" },
781
- el("span", { class: "ckw-msgr-card-sub ckw-msgr-recent-text" }, lineOf(last)),
782
- el("span", { class: "ckw-msgr-card-meta" }, `${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`)
783
- ),
784
- unseen.length ? el("span", { class: "ckw-msgr-badge" }, String(unseen.length)) : icon(I.next, 14)
785
- )
786
- );
787
- recent.addEventListener("click", () => go("messages"));
788
- parts.push(recent);
914
+ const running = chats.filter((c) => c.status !== "solved");
915
+ const rows = (running.length ? running : chats).slice(0, 2);
916
+ parts.push(...rows.map(chatRow));
917
+ if (chats.length > rows.length) {
918
+ const all = el("button", { type: "button", class: "ckw-msgr-all" }, tf("All conversations ({n})", { n: chats.length }), icon(I.next, 14));
919
+ all.addEventListener("click", () => {
920
+ enterMessages();
921
+ go("messages");
922
+ });
923
+ parts.push(all);
789
924
  }
790
925
  if (helpOn && hasArticles !== false) {
791
926
  const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" });
@@ -803,13 +938,13 @@ var build = (o) => {
803
938
  const groups = /* @__PURE__ */ new Map();
804
939
  for (const a of all) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
805
940
  const inside = !q && collection !== null ? groups.get(collection) ?? [] : null;
806
- const heading = q ? "Search results" : inside ? collection ?? "" : "Most read articles";
941
+ const heading = q ? t("Search results") : inside ? collection ?? "" : t("Most read articles");
807
942
  const parts = [el("h2", { class: "ckw-msgr-help-h" }, heading), helpSearch];
808
943
  if (articles === null) parts.push(el("div", { class: "ckw-skel" }));
809
944
  else if (!all.length) {
810
- const ask = el("button", { type: "button", class: "ckw-btn ckw-btn-primary" }, "Send us a message");
811
- ask.addEventListener("click", () => go("messages"));
812
- parts.push(el("div", { class: "ckw-msgr-empty" }, el("p", {}, q ? `Nothing found for \u201C${q}\u201D.` : "No help articles yet."), ask));
945
+ const ask = el("button", { type: "button", class: "ckw-btn ckw-btn-primary" }, t("Send us a message"));
946
+ ask.addEventListener("click", askAnew);
947
+ parts.push(el("div", { class: "ckw-msgr-empty" }, el("p", {}, q ? tf("Nothing found for \u201C{q}\u201D.", { q }) : t("No help articles yet.")), ask));
813
948
  } else if (q || inside)
814
949
  parts.push(
815
950
  el(
@@ -824,7 +959,7 @@ var build = (o) => {
824
959
  "div",
825
960
  { class: "ckw-msgr-hlist" },
826
961
  ...[...groups].map(
827
- ([name, items]) => helpRow(name, items.map((a) => a.title).join(", "), `${items.length} ${items.length === 1 ? "article" : "articles"}`, () => {
962
+ ([name, items]) => helpRow(name, items.map((a) => a.title).join(", "), items.length === 1 ? t("1 article") : tf("{n} articles", { n: items.length }), () => {
828
963
  collection = name;
829
964
  go("help");
830
965
  replay(body, "ckw-msgr-enter");
@@ -839,6 +974,14 @@ var build = (o) => {
839
974
  body.append(...parts.slice(2));
840
975
  }
841
976
  };
977
+ const drawUpdates = () => {
978
+ updatesNode ??= !updates ? el("div", { class: "ckw-skel" }) : !updates.length ? el("div", { class: "ckw-msgr-empty" }, el("p", {}, t("No updates yet."))) : el(
979
+ "div",
980
+ { class: "ckw-msgr-updates" },
981
+ ...updates.map((e) => el("article", { class: "ckw-msgr-update" }, el("p", { class: "ckw-msgr-doc-c" }, timeAgo(e.publishedAt)), el("h2", { class: "ckw-msgr-update-t" }, e.title), e.body ? el("p", { class: "ckw-msgr-update-b" }, e.body) : null))
982
+ );
983
+ if (body.firstChild !== updatesNode || body.childNodes.length !== 1) body.replaceChildren(updatesNode);
984
+ };
842
985
  let articleShown = null;
843
986
  const drawArticle = (id) => {
844
987
  if (articleShown?.id === id) return body.replaceChildren(articleShown.node);
@@ -848,26 +991,26 @@ var build = (o) => {
848
991
  api().article(id).then((a) => {
849
992
  const foot = el("div", { class: "ckw-msgr-rate" });
850
993
  const drawRate = () => {
851
- if (rated.has(id)) return foot.replaceChildren(el("span", {}, "Thanks for letting us know."));
852
- const yes = el("button", { type: "button", class: "ckw-btn" }, "Yes");
853
- const no = el("button", { type: "button", class: "ckw-btn" }, "No");
994
+ if (rated.has(id)) return foot.replaceChildren(el("span", {}, t("Thanks for letting us know.")));
995
+ const yes = el("button", { type: "button", class: "ckw-btn" }, t("Yes"));
996
+ const no = el("button", { type: "button", class: "ckw-btn" }, t("No"));
854
997
  const rate = (helpful) => {
855
998
  rated = new Set(rated).add(id);
856
999
  drawRate();
857
1000
  api().rateArticle(id, helpful).catch(() => void 0);
858
- if (!helpful) go("messages");
1001
+ if (!helpful) askAnew();
859
1002
  };
860
1003
  yes.addEventListener("click", () => rate(true));
861
1004
  no.addEventListener("click", () => rate(false));
862
- foot.replaceChildren(el("span", {}, "Did this answer your question?"), el("span", { class: "ckw-msgr-rate-b" }, yes, no));
1005
+ foot.replaceChildren(el("span", {}, t("Did this answer your question?")), el("span", { class: "ckw-msgr-rate-b" }, yes, no));
863
1006
  };
864
1007
  drawRate();
865
- const ask = el("button", { type: "button", class: "ckw-btn ckw-msgr-ask" }, icon(I.chat), "Still need help? Send us a message");
866
- ask.addEventListener("click", () => go("messages"));
1008
+ const ask = el("button", { type: "button", class: "ckw-btn ckw-msgr-ask" }, icon(I.chat), t("Still need help? Send us a message"));
1009
+ ask.addEventListener("click", askAnew);
867
1010
  node.replaceChildren(
868
1011
  el("p", { class: "ckw-msgr-doc-c" }, a.collection),
869
1012
  el("h2", { class: "ckw-msgr-doc-t" }, a.title),
870
- el("p", { class: "ckw-msgr-doc-c" }, `Updated ${timeAgo(a.updatedAt)}`),
1013
+ el("p", { class: "ckw-msgr-doc-c" }, tf("Updated {when}", { when: timeAgo(a.updatedAt) })),
871
1014
  el("div", { class: "ckw-msgr-doc-b" }, ...a.blocks.map(blockOf)),
872
1015
  foot,
873
1016
  ask
@@ -881,22 +1024,25 @@ var build = (o) => {
881
1024
  let listSig = "";
882
1025
  let listCount = -1;
883
1026
  const suggestions = el("div", { class: "ckw-msgr-sugg" });
1027
+ const outboxHere = () => outbox.filter((x) => x.draft === draft && x.chat === chatId);
884
1028
  const drawMessages = () => {
885
- const msgs = thread?.messages ?? [];
1029
+ const current = draft === null ? thread : null;
1030
+ const msgs = current?.messages ?? [];
1031
+ const here = outboxHere();
886
1032
  const parts = [];
887
- if (!loaded) parts.push(el("div", { class: "ckw-skel" }));
888
- else {
889
- const sig = `${msgs.map((m) => m.id).join(",")}|${pending ? `${pending.body}+${pending.attachments?.length ?? 0}` : ""}|${Math.floor(Date.now() / 6e4)}`;
1033
+ {
1034
+ const waiting = here.map((o2) => o2.message);
1035
+ const sig = `${msgs.map((m) => m.id).join(",")}|${waiting.map((m) => `${m.id}:${Date.now() - Date.parse(m.at) >= SLOW_SEND_MS}`).join(",")}|${Math.floor(Date.now() / 6e4)}`;
890
1036
  if (sig !== listSig) {
891
1037
  listSig = sig;
892
- const greet = el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting));
893
- const items = [greet, ...msgs.map((m) => bubble(m)), ...pending ? [bubble(pending, true)] : []];
1038
+ const greet = msgs.length || waiting.length || chatId ? [] : [el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting))];
1039
+ const items = [...greet, ...msgs.map((m) => bubble(m)), ...waiting.map((m) => bubble(m, true))];
894
1040
  if (listCount >= 0) items.slice(listCount).forEach((li) => li.setAttribute("data-new", ""));
895
1041
  listCount = items.length;
896
1042
  list.replaceChildren(...items);
897
1043
  }
898
1044
  parts.push(list);
899
- const top = !msgs.some((m) => m.from === "visitor") && !pending ? (articles ?? []).slice(0, 3) : [];
1045
+ const top = !msgs.some((m) => m.from === "visitor") && !here.length && !chatId ? (articles ?? []).slice(0, 3) : [];
900
1046
  if (top.length) {
901
1047
  if (suggestions.childNodes.length !== top.length)
902
1048
  suggestions.replaceChildren(
@@ -909,9 +1055,9 @@ var build = (o) => {
909
1055
  parts.push(suggestions);
910
1056
  }
911
1057
  }
912
- 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."));
1058
+ if (current?.conversation?.status === "solved" && !here.length) parts.push(el("p", { class: "ckw-fine ckw-msgr-note" }, t("This conversation is closed. Write to start a new one.")));
913
1059
  if (body.childNodes.length !== parts.length || parts.some((x, i) => body.childNodes[i] !== x)) body.replaceChildren(...parts);
914
- const count = msgs.length + (pending ? 1 : 0);
1060
+ const count = msgs.length + here.length;
915
1061
  if (count !== drawn) {
916
1062
  drawn = count;
917
1063
  body.scrollTop = body.scrollHeight;
@@ -932,24 +1078,35 @@ var build = (o) => {
932
1078
  pop.dataset.view = typeof view === "string" ? view : "article";
933
1079
  drawBar();
934
1080
  if (view === "home") drawHome();
935
- else if (view === "messages") drawMessages();
1081
+ else if (view === "messages") (listing ? drawChats : drawMessages)();
936
1082
  else if (view === "help") drawHelp();
1083
+ else if (view === "updates") drawUpdates();
937
1084
  else drawArticle(view.article);
938
1085
  const kids = view === "home" ? [sky, bar, body] : [bar, body];
939
- kids.push(view === "messages" ? form : tabs);
1086
+ kids.push(view === "messages" && !listing ? form : tabs);
940
1087
  if (o.branding !== false) kids.push(brand);
941
1088
  if (menu.isConnected) kids.push(menu);
942
1089
  if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);
943
1090
  };
944
1091
  const tick = async () => {
945
- ctrl?.abort();
946
- ctrl = new AbortController();
1092
+ if (ctrl) return;
1093
+ const mine = ctrl = new AbortController();
947
1094
  try {
948
- take(await api().chat(ctrl.signal));
1095
+ take(await api().chat(mine.signal, chatId ?? void 0));
949
1096
  error = "";
1097
+ refused = false;
950
1098
  } catch (e) {
951
1099
  if (e.name === "AbortError") return;
952
- if (open && view === "messages") error = e.message;
1100
+ const status = e.status;
1101
+ refused = status === 401 || status === 403;
1102
+ if (status === 404 && chatId) enterMessages();
1103
+ else if (open && view === "messages") error = sayable(e);
1104
+ } finally {
1105
+ if (ctrl === mine) ctrl = null;
1106
+ }
1107
+ if (!loaded) {
1108
+ listCount = -1;
1109
+ if (view === "messages" && chatId === null && draft === null && !text.value.trim() && !outbox.length && !picked.length) enterMessages();
953
1110
  }
954
1111
  loaded = true;
955
1112
  draw();
@@ -958,69 +1115,105 @@ var build = (o) => {
958
1115
  };
959
1116
  function schedule(now) {
960
1117
  clearTimeout(poll);
961
- const waiting = !!thread?.messages.some((m) => m.from === "visitor") && thread?.conversation?.status !== "solved";
962
- const wrote = (thread?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
963
- const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
964
- const ms = now ?? (watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : open || waiting ? POLL_IDLE_MS : 0);
965
- if (ms || now === 0) poll = setTimeout(() => void tick(), ms);
1118
+ const running = chats.filter((c) => c.status !== "solved");
1119
+ const waiting = running.length > 0;
1120
+ const recent = running.some((c) => c.last && Date.now() - new Date(c.last.at).getTime() < WAITING_FOR_MS);
1121
+ const quiet = now === void 0 && !open && !waiting;
1122
+ const ms = now ?? (document.visibilityState === "hidden" ? 0 : watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : quiet ? POLL_QUIET_MS : POLL_IDLE_MS);
1123
+ if (ms || now === 0) poll = setTimeout(() => quiet && (refused || !(o.user()?.id || chats.length)) ? schedule() : void tick(), ms);
966
1124
  }
967
1125
  const onVisible = () => {
968
1126
  if (document.visibilityState === "visible") schedule(0);
969
1127
  else clearTimeout(poll);
970
1128
  };
971
1129
  document.addEventListener("visibilitychange", onVisible);
972
- const submit = async () => {
1130
+ const flush = async () => {
1131
+ if (flushing) return;
1132
+ flushing = true;
1133
+ while (outbox.length) {
1134
+ const item = outbox[0];
1135
+ const { message, files, email: given } = item;
1136
+ await Promise.all(files.map((p) => p.up));
1137
+ const ids = files.filter((p) => p.id).map((p) => p.id);
1138
+ try {
1139
+ if (!message.body && !ids.length) throw new Error(error || t("The file could not be sent."));
1140
+ const sent = await api().sendChat({
1141
+ body: message.body,
1142
+ ...item.draft !== null ? { conversation: "new" } : item.chat ? { conversation: item.chat } : {},
1143
+ email: given,
1144
+ url: window.location.href.slice(0, 500),
1145
+ ...ids.length ? { attachments: ids } : {},
1146
+ // A little context for a bug report; the server reads browser and system from the user agent.
1147
+ context: { viewport: `${window.innerWidth}\xD7${window.innerHeight}` }
1148
+ });
1149
+ ctrl?.abort();
1150
+ ctrl = null;
1151
+ const went = sent.conversation?.id ?? null;
1152
+ if (went) {
1153
+ const after = outbox.slice(1).filter((x) => x.draft === item.draft && x.chat === item.chat);
1154
+ if (item.draft === draft && item.chat === chatId) {
1155
+ draft = null;
1156
+ chatId = went;
1157
+ }
1158
+ for (const x of after) {
1159
+ x.draft = null;
1160
+ x.chat = went;
1161
+ }
1162
+ }
1163
+ take(sent);
1164
+ outbox.shift();
1165
+ for (const p of files) {
1166
+ localUrls.delete(p.key);
1167
+ URL.revokeObjectURL(p.local);
1168
+ }
1169
+ } catch (e) {
1170
+ const back = outbox.splice(0);
1171
+ const gone = e.status === 404;
1172
+ enterChat(gone ? null : back[0].chat, gone ? ++drafts : back[0].draft);
1173
+ text.value = [...back.map((b) => b.message.body), text.value.trim()].filter(Boolean).join("\n");
1174
+ picked = [...back.flatMap((b) => b.files.filter((p) => p.id)), ...picked];
1175
+ for (const b of back) for (const p of b.files) localUrls.delete(p.key);
1176
+ error = gone ? t("This conversation is no longer available. Your message is still here to send as a new one.") : sayable(e);
1177
+ }
1178
+ draw();
1179
+ }
1180
+ flushing = false;
1181
+ schedule();
1182
+ };
1183
+ const submit = () => {
973
1184
  const msg = text.value.trim();
974
1185
  const files = picked;
975
- if (!msg && !files.length || pending || !emailOk()) return;
1186
+ if (!msg && !files.length || !emailOk()) return;
976
1187
  chime.unlock();
977
1188
  for (const p of files) localUrls.set(p.key, p.local);
978
- pending = {
979
- id: "pending",
980
- from: "visitor",
981
- body: msg,
982
- at: (/* @__PURE__ */ new Date()).toISOString(),
983
- ...files.length ? { attachments: files.map((p) => ({ id: p.key, name: p.name, type: p.type, size: p.size })) } : {}
984
- };
1189
+ outbox.push({
1190
+ message: {
1191
+ id: `pending-${++outboxSeq}`,
1192
+ from: "visitor",
1193
+ body: msg,
1194
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1195
+ ...files.length ? { attachments: files.map((p) => ({ id: p.key, name: p.name, type: p.type, size: p.size })) } : {}
1196
+ },
1197
+ files,
1198
+ email: needEmail() && email.value.trim() ? email.value.trim() : void 0,
1199
+ chat: chatId,
1200
+ draft
1201
+ });
985
1202
  text.value = "";
986
1203
  picked = [];
987
1204
  error = "";
988
1205
  draw();
989
- await Promise.all(files.map((p) => p.up));
990
- const ids = files.filter((p) => p.id).map((p) => p.id);
991
- let sent = false;
992
- try {
993
- if (!msg && !ids.length) throw new Error(error || "The file could not be sent.");
994
- take(
995
- await api().sendChat({
996
- body: msg,
997
- email: needEmail() && email.value.trim() ? email.value.trim() : void 0,
998
- url: window.location.href.slice(0, 500),
999
- ...ids.length ? { attachments: ids } : {},
1000
- // A little context for a bug report; the server reads browser and system from the user agent.
1001
- context: { viewport: `${window.innerWidth}\xD7${window.innerHeight}` }
1002
- })
1003
- );
1004
- sent = true;
1005
- } catch (e) {
1006
- text.value = msg;
1007
- picked = files.filter((p) => p.id);
1008
- error = e.message;
1009
- }
1010
- pending = null;
1011
- for (const p of files) localUrls.delete(p.key);
1012
- draw();
1013
- if (sent) for (const p of files) URL.revokeObjectURL(p.local);
1014
- schedule();
1206
+ setTimeout(draw, SLOW_SEND_MS + 50);
1207
+ void flush();
1015
1208
  };
1016
1209
  form.addEventListener("submit", (e) => {
1017
1210
  e.preventDefault();
1018
- void submit();
1211
+ submit();
1019
1212
  });
1020
1213
  text.addEventListener("keydown", (e) => {
1021
1214
  if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
1022
1215
  e.preventDefault();
1023
- void submit();
1216
+ submit();
1024
1217
  }
1025
1218
  });
1026
1219
  text.addEventListener("input", () => send.disabled = !canSend());
@@ -1049,7 +1242,7 @@ var build = (o) => {
1049
1242
  function show(v = "home", still = false) {
1050
1243
  const was = open;
1051
1244
  open = true;
1052
- view = v === "help" && !helpOn ? "home" : v;
1245
+ view = v === "help" && !helpOn || v === "updates" && !updatesOn ? "home" : v;
1053
1246
  drawn = -1;
1054
1247
  previewGone = true;
1055
1248
  pop.toggleAttribute("data-still", still);
@@ -1090,10 +1283,14 @@ var build = (o) => {
1090
1283
  document.addEventListener("keydown", onKey);
1091
1284
  void tick();
1092
1285
  if (helpOn) void loadArticles();
1286
+ if (updatesOn) void loadUpdates();
1093
1287
  drawFab();
1094
1288
  if (o.opened) show(o.opened, true);
1095
1289
  return {
1096
- open: (v) => show(v ?? "home"),
1290
+ open: (v) => {
1291
+ if (v === "messages") enterMessages();
1292
+ show(v ?? "home");
1293
+ },
1097
1294
  close,
1098
1295
  toggle: () => open ? close() : show(),
1099
1296
  isOpen: () => open,
@@ -1102,7 +1299,7 @@ var build = (o) => {
1102
1299
  },
1103
1300
  destroy: () => {
1104
1301
  clearTimeout(poll);
1105
- for (const t of leaving.values()) clearTimeout(t);
1302
+ for (const t2 of leaving.values()) clearTimeout(t2);
1106
1303
  ctrl?.abort();
1107
1304
  for (const p of picked) forget(p);
1108
1305
  for (const u of localUrls.values()) URL.revokeObjectURL(u);