@mrrocket/sdk 0.18.0 → 0.22.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 (63) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +36 -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 +8 -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 +17 -1
  13. package/dist/client.js +25 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/core.d.ts +5 -1
  16. package/dist/core.js +1 -1
  17. package/dist/core.js.map +1 -1
  18. package/dist/feedback.js +161 -119
  19. package/dist/feedback.js.map +1 -1
  20. package/dist/flows-ui.d.ts +1 -1
  21. package/dist/flows-ui.js +100 -72
  22. package/dist/flows-ui.js.map +1 -1
  23. package/dist/flows-web.js +2 -1
  24. package/dist/flows-web.js.map +1 -1
  25. package/dist/flows.d.ts +38 -6
  26. package/dist/flows.js +51 -17
  27. package/dist/flows.js.map +1 -1
  28. package/dist/i18n.d.ts +22 -0
  29. package/dist/i18n.js +74 -0
  30. package/dist/i18n.js.map +1 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/messenger-ui.d.ts +9 -3
  35. package/dist/messenger-ui.js +324 -152
  36. package/dist/messenger-ui.js.map +1 -1
  37. package/dist/messenger.d.ts +3 -1
  38. package/dist/messenger.js +3 -2
  39. package/dist/messenger.js.map +1 -1
  40. package/dist/onboarding-report.d.ts +19 -0
  41. package/dist/onboarding-report.js +23 -0
  42. package/dist/onboarding-report.js.map +1 -0
  43. package/dist/onboarding-state.d.ts +3 -0
  44. package/dist/onboarding-state.js +7 -1
  45. package/dist/onboarding-state.js.map +1 -1
  46. package/dist/paywall.js +2 -1
  47. package/dist/paywall.js.map +1 -1
  48. package/dist/react-native/index.d.cts +93 -8
  49. package/dist/react-native/index.d.ts +93 -8
  50. package/dist/react-native/index.js +819 -511
  51. package/dist/react-native/index.js.map +1 -1
  52. package/dist/server.d.ts +4 -2
  53. package/dist/server.js +2 -2
  54. package/dist/server.js.map +1 -1
  55. package/dist/styles-css.d.ts +1 -1
  56. package/dist/styles-css.js +35 -2
  57. package/dist/styles-css.js.map +1 -1
  58. package/dist/survey.js +18 -14
  59. package/dist/survey.js.map +1 -1
  60. package/dist/ui.d.ts +2 -0
  61. package/dist/ui.js +15 -12
  62. package/dist/ui.js.map +1 -1
  63. package/package.json +6 -3
@@ -8,36 +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;
16
19
  var SLOW_SEND_MS = 1500;
17
20
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
18
21
  var SEEN_KEY = "ckw_msgr_seen";
22
+ var SEEN_CHATS_KEY = "ckw_msgr_seen_chats";
19
23
  var readSeen = () => {
20
24
  try {
21
- return window.localStorage.getItem(SEEN_KEY) ?? "";
25
+ return { old: window.localStorage.getItem(SEEN_KEY) ?? "", chats: JSON.parse(window.localStorage.getItem(SEEN_CHATS_KEY) ?? "{}") };
22
26
  } catch {
23
- return "";
27
+ return { old: "", chats: {} };
24
28
  }
25
29
  };
26
- var writeSeen = (id) => {
30
+ var writeSeen = (chats) => {
27
31
  try {
28
- window.localStorage.setItem(SEEN_KEY, id);
32
+ window.localStorage.setItem(SEEN_CHATS_KEY, JSON.stringify(chats));
29
33
  } catch {
30
34
  }
31
35
  };
32
36
  var timeAgo = (iso) => {
33
- const t = new Date(iso).getTime();
34
- if (Number.isNaN(t)) return "";
35
- const d = Math.max(0, Date.now() - t) / 1e3;
36
- if (d < 60) return "just now";
37
- if (d < 3600) return `${Math.floor(d / 60)}m ago`;
38
- if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
39
- if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;
40
- 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" });
41
45
  };
42
46
  var el = (tag, attrs = {}, ...kids) => {
43
47
  const e = document.createElement(tag);
@@ -72,11 +76,12 @@ var I = {
72
76
  minus: "M5 12h14",
73
77
  down: "m6 9 6 6 6-6",
74
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",
75
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",
76
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",
77
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"
78
83
  };
79
- 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 }) : "");
80
85
  var canShoot = () => typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
81
86
  var BUBBLE = "M7.9 20A9 9 0 1 0 4 16.1L2 22z";
82
87
  var filled = (d, size) => {
@@ -100,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
100
105
  var SMILE = "M9.5 15.5c1.4 1 3.6 1 5 0";
101
106
  var DISC = "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z";
102
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";
103
109
  var chime = /* @__PURE__ */ (() => {
104
110
  let ctx = null;
105
111
  const get = () => {
@@ -121,11 +127,11 @@ var chime = /* @__PURE__ */ (() => {
121
127
  try {
122
128
  const c = get();
123
129
  if (!c || c.state !== "running") return;
124
- const t = c.currentTime;
130
+ const t2 = c.currentTime;
125
131
  [880, 1318.5].forEach((f, i) => {
126
132
  const o = c.createOscillator();
127
133
  const g = c.createGain();
128
- const at = t + i * 0.11;
134
+ const at = t2 + i * 0.11;
129
135
  o.type = "sine";
130
136
  o.frequency.value = f;
131
137
  g.gain.setValueAtTime(1e-4, at);
@@ -190,9 +196,11 @@ var mountMessenger = (o) => {
190
196
  const later = (fn) => real ? fn(real) : queued.push(fn);
191
197
  const ctrl = new AbortController();
192
198
  const caching = !o.client && !!o.apiKey;
199
+ let lastLook = null;
193
200
  const start = (look) => {
194
201
  if (gone) return;
195
- const key = JSON.stringify(look ?? {});
202
+ lastLook = look;
203
+ const key = `${wordsVersion()}:${JSON.stringify(look ?? {})}`;
196
204
  if (real && key === shown) return;
197
205
  if (real?.isOpen()) {
198
206
  real.onClose(() => start(look));
@@ -207,18 +215,27 @@ var mountMessenger = (o) => {
207
215
  const cached = caching ? readCachedLook(o.apiKey) : null;
208
216
  if (cached) start(cached);
209
217
  const timer = cached ? void 0 : setTimeout(() => start(null), LOOK_WAIT_MS);
210
- (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) => {
211
219
  if (caching && look) writeCachedLook(o.apiKey, look);
212
220
  start(look);
213
221
  }).catch(() => {
214
- if (!real) start(null);
222
+ start(lastLook);
215
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
+ });
216
232
  return {
217
233
  open: (v) => later((h) => h.open(v)),
218
234
  close: () => real?.close(),
219
235
  toggle: () => later((h) => h.toggle()),
220
236
  destroy: () => {
221
237
  gone = true;
238
+ offWords();
222
239
  ctrl.abort();
223
240
  clearTimeout(timer);
224
241
  real?.destroy();
@@ -229,15 +246,16 @@ var PLACES = ["top-left", "top-center", "top-right", "middle-left", "middle-righ
229
246
  var build = (o) => {
230
247
  injectStyles();
231
248
  const position = PLACES.find((p) => p === o.position) ?? "bottom-right";
232
- const title = o.title || "Support";
233
- 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?");
234
251
  const askEmail = o.askEmail ?? true;
235
252
  const label = o.label ?? "";
236
253
  const reply = replyLine(o.replyTime);
237
254
  const initial = (title.trim()[0] ?? "?").toUpperCase();
238
- 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") };
239
256
  const launcherOn = o.launcher ?? true;
240
257
  const helpOn = o.help ?? true;
258
+ const updatesOn = o.updates ?? true;
241
259
  const soundOn = o.sound ?? true;
242
260
  const previewOn = o.preview ?? true;
243
261
  const api = () => o.client ?? createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user(), userHash: o.userHash?.() });
@@ -246,7 +264,9 @@ var build = (o) => {
246
264
  "data-theme": o.theme ?? "auto",
247
265
  "data-corners": o.corners === "soft" || o.corners === "square" ? o.corners : "round",
248
266
  "data-font": o.font === "system" ? "system" : "site",
249
- "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() } : {}
250
270
  });
251
271
  if (o.container) root.setAttribute("data-contained", "");
252
272
  if (o.accent) {
@@ -259,52 +279,96 @@ var build = (o) => {
259
279
  let afterClose = null;
260
280
  let view = "home";
261
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;
262
288
  let loaded = false;
263
289
  let error = "";
264
- const sayable = (e) => e.status === 401 ? "The chat is not available right now. Please try again later." : e.message;
290
+ const sayable = (e) => e.status === 401 ? t("The chat is not available right now. Please try again later.") : e.message;
265
291
  let outbox = [];
266
292
  let outboxSeq = 0;
267
293
  let flushing = false;
268
- let lastKnown = null;
294
+ const known = /* @__PURE__ */ new Map();
295
+ let seen = readSeen();
269
296
  let unseen = [];
270
297
  let previewGone = false;
271
298
  let poll;
299
+ let refused = false;
272
300
  let ctrl = null;
273
301
  let articles = null;
274
302
  let query = "";
275
303
  let rated = /* @__PURE__ */ new Set();
276
304
  let hasArticles = null;
305
+ let updates = null;
306
+ let updatesNode = null;
277
307
  let collection = null;
278
308
  let wide = false;
279
- const teamMsgs = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
280
- const lastTeamId = (t) => teamMsgs(t).at(-1)?.id ?? "";
309
+ const isAnswer = (m) => m.from === "team" || m.from === "ai";
281
310
  const watching = () => open && view === "messages" && document.visibilityState !== "hidden";
282
- const take = (t) => {
283
- const team = teamMsgs(t);
284
- if (lastKnown === null) {
285
- const seen = readSeen();
286
- const i = team.findIndex((m) => m.id === seen);
287
- 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
+ }
288
324
  } else {
289
- const i = team.findIndex((m) => m.id === lastKnown);
290
- const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
325
+ const fresh = after(was);
291
326
  if (fresh.length) {
292
327
  if (soundOn) chime.play();
293
- if (!watching()) {
294
- unseen = [...unseen, ...fresh];
328
+ if (!watching() || shownChat() !== chat) {
329
+ unseen = [...unseen, ...fresh.map((m) => ({ ...m, chat }))];
295
330
  previewGone = false;
296
331
  }
297
332
  }
298
333
  }
299
- lastKnown = lastTeamId(t);
300
- 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;
301
346
  if (watching()) markSeen();
302
347
  };
303
348
  const markSeen = () => {
304
- unseen = [];
305
- const id = lastTeamId(thread);
306
- 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);
307
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);
308
372
  const letter = (size) => el("span", { class: `ckw-msgr-av${size ? " ckw-msgr-av-sm" : ""}`, "aria-hidden": "true" }, initial);
309
373
  const avatar = (size) => {
310
374
  if (!o.icon) return letter(size);
@@ -365,14 +429,14 @@ var build = (o) => {
365
429
  if (!fabCount.isConnected) fab.append(fabCount);
366
430
  } else fabCount.remove();
367
431
  fab.setAttribute("aria-expanded", String(open));
368
- 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"));
369
433
  drawPreview();
370
434
  };
371
435
  const previewShown = () => previewOn && !open && !previewGone && !!unseen.length && launcherOn;
372
436
  const drawPreview = () => {
373
437
  const m = unseen.at(-1);
374
438
  if (!previewShown() || !m) return leave(prev);
375
- 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));
376
440
  x.addEventListener("click", (e) => {
377
441
  e.stopPropagation();
378
442
  previewGone = true;
@@ -384,18 +448,23 @@ var build = (o) => {
384
448
  el(
385
449
  "span",
386
450
  { class: "ckw-msgr-preview-main" },
387
- 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 })}` : ""}`),
388
452
  el("span", { class: "ckw-msgr-preview-text" }, lineOf(m))
389
453
  )
390
454
  );
391
- card.addEventListener("click", () => show("messages"));
455
+ card.addEventListener("click", () => {
456
+ enterChat(m.chat, null);
457
+ show("messages");
458
+ });
392
459
  prev.replaceChildren(card, x);
393
460
  enter(prev);
394
461
  };
395
462
  fab.addEventListener("click", () => {
396
463
  chime.unlock();
397
- if (open) close();
398
- 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");
399
468
  });
400
469
  if (launcherOn) root.appendChild(fab);
401
470
  const pop = el("div", { class: "ckw-pop ckw-msgr", "data-pos": position, role: "dialog", "aria-label": title });
@@ -404,21 +473,23 @@ var build = (o) => {
404
473
  pop.addEventListener("click", () => chime.unlock());
405
474
  const lead = el("span", { class: "ckw-msgr-lead" });
406
475
  const mid = el("span", { class: "ckw-msgr-mid" });
407
- const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": "Messenger" });
476
+ const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": t("Messenger") });
408
477
  const tab = (v, name, line, shape, cut) => {
409
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));
410
479
  b.addEventListener("click", () => {
411
480
  collection = null;
481
+ if (v === "messages") enterMessages();
412
482
  go(v);
413
483
  });
414
484
  return b;
415
485
  };
416
- const homeTab = tab("home", "Home", I.home, HOUSE, SMILE);
417
- const chatTab = tab("messages", "Messages", I.chat, BUBBLE, DOTS);
418
- 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, "");
419
490
  const tabCount = el("span", { class: "ckw-msgr-count ckw-msgr-count-tab", "aria-hidden": "true" });
420
- const menuBtn = el("button", { type: "button", class: "ckw-msgr-ib", "aria-label": "More", "aria-haspopup": "menu", "aria-expanded": "false" }, icon(I.dots, 18));
421
- 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));
422
493
  xBtn.addEventListener("click", () => close());
423
494
  bar.append(lead, mid, el("span", { class: "ckw-msgr-trail" }, menuBtn, xBtn));
424
495
  const menu = el("div", { class: "ckw-msgr-menu", role: "menu" });
@@ -440,9 +511,9 @@ var build = (o) => {
440
511
  e.stopPropagation();
441
512
  if (menuOpen()) return closeMenu();
442
513
  menu.replaceChildren(
443
- el("span", { class: "ckw-msgr-menu-h" }, "Quick actions"),
444
- menuItem(I.minus, "Minimize chat", () => close()),
445
- 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"), () => {
446
517
  wide = !wide;
447
518
  pop.toggleAttribute("data-wide", wide);
448
519
  })
@@ -460,26 +531,37 @@ var build = (o) => {
460
531
  };
461
532
  if (typeof view !== "string")
462
533
  lead.replaceChildren(
463
- back(() => go("help"), "Back to help")
534
+ back(() => go("help"), t("Back to help"))
464
535
  );
465
536
  else if (view === "help" && collection && !query.trim())
466
537
  lead.replaceChildren(
467
538
  back(() => {
468
539
  collection = null;
469
540
  go("help");
470
- }, "All collections")
541
+ }, t("All collections"))
542
+ );
543
+ else if (view === "messages" && !listing && chats.length > 1)
544
+ lead.replaceChildren(
545
+ back(() => {
546
+ enterMessages();
547
+ go("messages");
548
+ }, t("All conversations"))
471
549
  );
472
- else if (view === "messages") lead.replaceChildren(back(() => go("home"), "Back to home"));
550
+ else if (view === "messages") lead.replaceChildren(back(() => go("home"), t("Back to home")));
473
551
  else if (view === "home") lead.replaceChildren(logo);
474
552
  else lead.replaceChildren();
475
- if (view === "messages") mid.replaceChildren(avatar("sm"), el("span", { class: "ckw-msgr-mid-t" }, el("b", {}, title), el("span", { class: "ckw-msgr-online" }, reply)));
476
- 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")));
477
557
  else mid.replaceChildren();
478
- const items = helpOn && hasArticles !== false ? [homeTab, chatTab, helpTab] : [homeTab, chatTab];
479
- 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);
480
561
  homeTab.setAttribute("aria-current", view === "home" ? "page" : "false");
481
562
  chatTab.setAttribute("aria-current", view === "messages" ? "page" : "false");
482
- 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");
483
565
  if (unseen.length && view !== "messages") {
484
566
  tabCount.textContent = String(Math.min(unseen.length, 9));
485
567
  if (!tabCount.isConnected) chatTab.firstChild.appendChild(tabCount);
@@ -487,14 +569,14 @@ var build = (o) => {
487
569
  };
488
570
  const form = el("form", { class: "ckw-msgr-compose" });
489
571
  const err = el("p", { class: "ckw-msgr-error", role: "alert" });
490
- 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" });
491
573
  const row = el("div", { class: "ckw-msgr-box" });
492
- const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write your message\u2026", "aria-label": "Message", maxlength: "4000" });
493
- 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));
494
576
  const shots = canShoot();
495
577
  const clip = el(
496
578
  "button",
497
- { 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" } : {} },
498
580
  icon(I.clip, 18)
499
581
  );
500
582
  row.append(text, el("div", { class: "ckw-msgr-tools" }, clip, el("span", { class: "ckw-msgr-sp" }), send));
@@ -502,7 +584,7 @@ var build = (o) => {
502
584
  form.append(row);
503
585
  const picker = el("input", { type: "file", accept: CHAT_FILE_TYPES.join(","), multiple: "", hidden: "", tabindex: "-1", "aria-hidden": "true" });
504
586
  form.append(picker);
505
- 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") });
506
588
  let picked = [];
507
589
  const localUrls = /* @__PURE__ */ new Map();
508
590
  const fileUrl = (id) => localUrls.get(id) ?? api().attachmentUrl(id);
@@ -519,15 +601,15 @@ var build = (o) => {
519
601
  for (const f of list2) {
520
602
  const name = f.name || "screenshot.png";
521
603
  if (picked.length >= CHAT_FILES_PER_MESSAGE) {
522
- error = `Up to ${CHAT_FILES_PER_MESSAGE} files per message.`;
604
+ error = tf("Up to {n} files per message.", { n: CHAT_FILES_PER_MESSAGE });
523
605
  break;
524
606
  }
525
607
  if (!CHAT_FILE_TYPES.includes(f.type)) {
526
- 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.");
527
609
  continue;
528
610
  }
529
611
  if (f.size > CHAT_FILE_MAX_BYTES) {
530
- error = `${name} is larger than 10 MB.`;
612
+ error = tf("{name} is larger than 10 MB.", { name });
531
613
  continue;
532
614
  }
533
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() };
@@ -545,8 +627,10 @@ var build = (o) => {
545
627
  );
546
628
  picked.push(p);
547
629
  }
548
- if (open && view !== "messages") go("messages");
549
- else draw();
630
+ if (open && (view !== "messages" || listing)) {
631
+ startChat();
632
+ go("messages");
633
+ } else draw();
550
634
  text.focus();
551
635
  };
552
636
  let traySig = "";
@@ -556,14 +640,14 @@ var build = (o) => {
556
640
  traySig = sig;
557
641
  tray.replaceChildren(
558
642
  ...picked.map((p) => {
559
- 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));
560
644
  x.addEventListener("click", () => {
561
645
  dropFile(p);
562
646
  draw();
563
647
  text.focus();
564
648
  });
565
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));
566
- 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);
567
651
  })
568
652
  );
569
653
  }
@@ -576,7 +660,7 @@ var build = (o) => {
576
660
  stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "browser" }, audio: false, preferCurrentTab: true, selfBrowserSurface: "include" });
577
661
  } catch (e) {
578
662
  if (e.name !== "NotAllowedError" && e.name !== "AbortError") {
579
- error = "The screenshot did not work here. Attach a picture instead.";
663
+ error = t("The screenshot did not work here. Attach a picture instead.");
580
664
  draw();
581
665
  }
582
666
  return;
@@ -602,11 +686,11 @@ var build = (o) => {
602
686
  } catch {
603
687
  blob = null;
604
688
  } finally {
605
- for (const t of stream.getTracks()) t.stop();
689
+ for (const t2 of stream.getTracks()) t2.stop();
606
690
  root.style.visibility = "";
607
691
  }
608
692
  if (!blob || !blob.size) {
609
- error = "The screenshot did not work here. Attach a picture instead.";
693
+ error = t("The screenshot did not work here. Attach a picture instead.");
610
694
  return draw();
611
695
  }
612
696
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-");
@@ -633,8 +717,8 @@ var build = (o) => {
633
717
  if (!shots) return picker.click();
634
718
  if (amenuOpen()) return closeAmenu();
635
719
  amenu.replaceChildren(
636
- amenuItem(I.image, "Photo or file", () => picker.click()),
637
- 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())
638
722
  );
639
723
  clip.setAttribute("aria-expanded", "true");
640
724
  enter(amenu, form);
@@ -644,9 +728,9 @@ var build = (o) => {
644
728
  addFiles([...picker.files ?? []]);
645
729
  picker.value = "";
646
730
  });
647
- const brand = el("a", { class: "ckw-msgr-powered", href: BRAND_URL, target: "_blank", rel: "noopener" }, "Powered by ", el("b", {}, "MRRocket"));
648
- const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": "Messages" });
649
- 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;
650
734
  const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());
651
735
  const canSend = () => (!!text.value.trim() || picked.length > 0) && emailOk();
652
736
  const go = (v) => {
@@ -688,7 +772,7 @@ var build = (o) => {
688
772
  chip.addEventListener("click", () => go({ article: a.id }));
689
773
  li.append(chip);
690
774
  }
691
- li.append(el("span", { class: "ckw-msg-meta" }, m.from === "ai" ? agentFace() : null, `${m.from !== "visitor" ? `${whoLabel[m.from]} \xB7 ` : ""}${slow ? "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)}`));
692
776
  return li;
693
777
  };
694
778
  const articleRow = (a) => {
@@ -706,15 +790,15 @@ var build = (o) => {
706
790
  b.addEventListener("click", act);
707
791
  return b;
708
792
  };
709
- const searchBox = (placeholder = "Search for help") => {
793
+ const searchBox = (placeholder = t("Search for help")) => {
710
794
  const wrap = el("label", { class: "ckw-msgr-search" });
711
- 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" });
712
796
  input.value = query;
713
- let t;
797
+ let wait;
714
798
  input.addEventListener("input", () => {
715
799
  query = input.value;
716
- clearTimeout(t);
717
- t = setTimeout(() => {
800
+ clearTimeout(wait);
801
+ wait = setTimeout(() => {
718
802
  if (view !== "help") return go("help");
719
803
  void loadArticles(true);
720
804
  }, 250);
@@ -728,9 +812,13 @@ var build = (o) => {
728
812
  wrap.append(icon(I.search, 16), input);
729
813
  return wrap;
730
814
  };
731
- const helpSearch = searchBox("Search our help center\u2026");
815
+ const helpSearch = searchBox(t("Search our help center\u2026"));
732
816
  const helpInput = helpSearch.querySelector("input");
733
817
  let loadingArticles = false;
818
+ const askAnew = () => {
819
+ startChat();
820
+ go("messages");
821
+ };
734
822
  const loadArticles = async (force = false) => {
735
823
  if (!helpOn || loadingArticles && !force) return;
736
824
  loadingArticles = true;
@@ -747,50 +835,92 @@ var build = (o) => {
747
835
  }
748
836
  draw();
749
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
+ };
750
847
  const askForm = el("form", { class: "ckw-msgr-askq" });
751
- const askIn = el("input", { class: "ckw-msgr-askq-in", placeholder: "Ask a question", "aria-label": "Ask a question", maxlength: "4000", autocomplete: "off" });
752
- 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));
753
850
  askForm.append(askIn, askGo);
754
851
  askIn.addEventListener("input", () => askGo.disabled = !askIn.value.trim());
755
852
  askForm.addEventListener("submit", (e) => {
756
853
  e.preventDefault();
757
- if (!askIn.value.trim()) return go("messages");
854
+ if (!askIn.value.trim()) {
855
+ enterMessages();
856
+ return go("messages");
857
+ }
758
858
  text.value = askIn.value;
759
859
  askIn.value = "";
760
860
  askGo.disabled = true;
861
+ startChat();
761
862
  go("messages");
762
863
  if (canSend()) submit();
763
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
+ };
764
904
  const drawHome = () => {
765
905
  const first = o.user()?.name?.trim().split(/\s+/)[0];
766
- 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?");
767
907
  const hero = el(
768
908
  "h2",
769
909
  { class: "ckw-msgr-hero ckw-msgr-hello" },
770
- 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}")),
771
911
  el("span", { class: "ckw-msgr-hello-sub" }, rest)
772
912
  );
773
- const last = (thread?.messages ?? []).at(-1);
774
913
  const parts = [hero, askForm];
775
- if (last) {
776
- const recent = el(
777
- "button",
778
- { type: "button", class: "ckw-msgr-card ckw-msgr-recent", "aria-label": `Recent message: ${lineOf(last)}` },
779
- el(
780
- "span",
781
- { class: "ckw-msgr-recent-row" },
782
- avatar("sm"),
783
- el(
784
- "span",
785
- { class: "ckw-msgr-card-main" },
786
- el("span", { class: "ckw-msgr-card-sub ckw-msgr-recent-text" }, lineOf(last)),
787
- el("span", { class: "ckw-msgr-card-meta" }, `${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`)
788
- ),
789
- unseen.length ? el("span", { class: "ckw-msgr-badge" }, String(unseen.length)) : icon(I.next, 14)
790
- )
791
- );
792
- recent.addEventListener("click", () => go("messages"));
793
- 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);
794
924
  }
795
925
  if (helpOn && hasArticles !== false) {
796
926
  const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" });
@@ -808,13 +938,13 @@ var build = (o) => {
808
938
  const groups = /* @__PURE__ */ new Map();
809
939
  for (const a of all) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
810
940
  const inside = !q && collection !== null ? groups.get(collection) ?? [] : null;
811
- const heading = q ? "Search results" : inside ? collection ?? "" : "Most read articles";
941
+ const heading = q ? t("Search results") : inside ? collection ?? "" : t("Most read articles");
812
942
  const parts = [el("h2", { class: "ckw-msgr-help-h" }, heading), helpSearch];
813
943
  if (articles === null) parts.push(el("div", { class: "ckw-skel" }));
814
944
  else if (!all.length) {
815
- const ask = el("button", { type: "button", class: "ckw-btn ckw-btn-primary" }, "Send us a message");
816
- ask.addEventListener("click", () => go("messages"));
817
- 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));
818
948
  } else if (q || inside)
819
949
  parts.push(
820
950
  el(
@@ -829,7 +959,7 @@ var build = (o) => {
829
959
  "div",
830
960
  { class: "ckw-msgr-hlist" },
831
961
  ...[...groups].map(
832
- ([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 }), () => {
833
963
  collection = name;
834
964
  go("help");
835
965
  replay(body, "ckw-msgr-enter");
@@ -844,6 +974,14 @@ var build = (o) => {
844
974
  body.append(...parts.slice(2));
845
975
  }
846
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
+ };
847
985
  let articleShown = null;
848
986
  const drawArticle = (id) => {
849
987
  if (articleShown?.id === id) return body.replaceChildren(articleShown.node);
@@ -853,26 +991,26 @@ var build = (o) => {
853
991
  api().article(id).then((a) => {
854
992
  const foot = el("div", { class: "ckw-msgr-rate" });
855
993
  const drawRate = () => {
856
- if (rated.has(id)) return foot.replaceChildren(el("span", {}, "Thanks for letting us know."));
857
- const yes = el("button", { type: "button", class: "ckw-btn" }, "Yes");
858
- 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"));
859
997
  const rate = (helpful) => {
860
998
  rated = new Set(rated).add(id);
861
999
  drawRate();
862
1000
  api().rateArticle(id, helpful).catch(() => void 0);
863
- if (!helpful) go("messages");
1001
+ if (!helpful) askAnew();
864
1002
  };
865
1003
  yes.addEventListener("click", () => rate(true));
866
1004
  no.addEventListener("click", () => rate(false));
867
- 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));
868
1006
  };
869
1007
  drawRate();
870
- const ask = el("button", { type: "button", class: "ckw-btn ckw-msgr-ask" }, icon(I.chat), "Still need help? Send us a message");
871
- 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);
872
1010
  node.replaceChildren(
873
1011
  el("p", { class: "ckw-msgr-doc-c" }, a.collection),
874
1012
  el("h2", { class: "ckw-msgr-doc-t" }, a.title),
875
- 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) })),
876
1014
  el("div", { class: "ckw-msgr-doc-b" }, ...a.blocks.map(blockOf)),
877
1015
  foot,
878
1016
  ask
@@ -886,22 +1024,25 @@ var build = (o) => {
886
1024
  let listSig = "";
887
1025
  let listCount = -1;
888
1026
  const suggestions = el("div", { class: "ckw-msgr-sugg" });
1027
+ const outboxHere = () => outbox.filter((x) => x.draft === draft && x.chat === chatId);
889
1028
  const drawMessages = () => {
890
- const msgs = thread?.messages ?? [];
1029
+ const current = draft === null ? thread : null;
1030
+ const msgs = current?.messages ?? [];
1031
+ const here = outboxHere();
891
1032
  const parts = [];
892
1033
  {
893
- const waiting = outbox.map((o2) => o2.message);
1034
+ const waiting = here.map((o2) => o2.message);
894
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)}`;
895
1036
  if (sig !== listSig) {
896
1037
  listSig = sig;
897
- const greet = msgs.length || waiting.length ? [] : [el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting))];
1038
+ const greet = msgs.length || waiting.length || chatId ? [] : [el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting))];
898
1039
  const items = [...greet, ...msgs.map((m) => bubble(m)), ...waiting.map((m) => bubble(m, true))];
899
1040
  if (listCount >= 0) items.slice(listCount).forEach((li) => li.setAttribute("data-new", ""));
900
1041
  listCount = items.length;
901
1042
  list.replaceChildren(...items);
902
1043
  }
903
1044
  parts.push(list);
904
- const top = !msgs.some((m) => m.from === "visitor") && !outbox.length ? (articles ?? []).slice(0, 3) : [];
1045
+ const top = !msgs.some((m) => m.from === "visitor") && !here.length && !chatId ? (articles ?? []).slice(0, 3) : [];
905
1046
  if (top.length) {
906
1047
  if (suggestions.childNodes.length !== top.length)
907
1048
  suggestions.replaceChildren(
@@ -914,9 +1055,9 @@ var build = (o) => {
914
1055
  parts.push(suggestions);
915
1056
  }
916
1057
  }
917
- if (thread?.conversation?.status === "solved" && !outbox.length) 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.")));
918
1059
  if (body.childNodes.length !== parts.length || parts.some((x, i) => body.childNodes[i] !== x)) body.replaceChildren(...parts);
919
- const count = msgs.length + outbox.length;
1060
+ const count = msgs.length + here.length;
920
1061
  if (count !== drawn) {
921
1062
  drawn = count;
922
1063
  body.scrollTop = body.scrollHeight;
@@ -937,11 +1078,12 @@ var build = (o) => {
937
1078
  pop.dataset.view = typeof view === "string" ? view : "article";
938
1079
  drawBar();
939
1080
  if (view === "home") drawHome();
940
- else if (view === "messages") drawMessages();
1081
+ else if (view === "messages") (listing ? drawChats : drawMessages)();
941
1082
  else if (view === "help") drawHelp();
1083
+ else if (view === "updates") drawUpdates();
942
1084
  else drawArticle(view.article);
943
1085
  const kids = view === "home" ? [sky, bar, body] : [bar, body];
944
- kids.push(view === "messages" ? form : tabs);
1086
+ kids.push(view === "messages" && !listing ? form : tabs);
945
1087
  if (o.branding !== false) kids.push(brand);
946
1088
  if (menu.isConnected) kids.push(menu);
947
1089
  if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);
@@ -950,15 +1092,22 @@ var build = (o) => {
950
1092
  if (ctrl) return;
951
1093
  const mine = ctrl = new AbortController();
952
1094
  try {
953
- take(await api().chat(mine.signal));
1095
+ take(await api().chat(mine.signal, chatId ?? void 0));
954
1096
  error = "";
1097
+ refused = false;
955
1098
  } catch (e) {
956
1099
  if (e.name === "AbortError") return;
957
- if (open && view === "messages") error = sayable(e);
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);
958
1104
  } finally {
959
1105
  if (ctrl === mine) ctrl = null;
960
1106
  }
961
- if (!loaded) listCount = -1;
1107
+ if (!loaded) {
1108
+ listCount = -1;
1109
+ if (view === "messages" && chatId === null && draft === null && !text.value.trim() && !outbox.length && !picked.length) enterMessages();
1110
+ }
962
1111
  loaded = true;
963
1112
  draw();
964
1113
  drawFab();
@@ -966,11 +1115,12 @@ var build = (o) => {
966
1115
  };
967
1116
  function schedule(now) {
968
1117
  clearTimeout(poll);
969
- const waiting = !!thread?.messages.some((m) => m.from === "visitor") && thread?.conversation?.status !== "solved";
970
- const wrote = (thread?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
971
- const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
972
- const ms = now ?? (watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : open || waiting ? POLL_IDLE_MS : 0);
973
- 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);
974
1124
  }
975
1125
  const onVisible = () => {
976
1126
  if (document.visibilityState === "visible") schedule(0);
@@ -981,13 +1131,15 @@ var build = (o) => {
981
1131
  if (flushing) return;
982
1132
  flushing = true;
983
1133
  while (outbox.length) {
984
- const { message, files, email: given } = outbox[0];
1134
+ const item = outbox[0];
1135
+ const { message, files, email: given } = item;
985
1136
  await Promise.all(files.map((p) => p.up));
986
1137
  const ids = files.filter((p) => p.id).map((p) => p.id);
987
1138
  try {
988
- if (!message.body && !ids.length) throw new Error(error || "The file could not be sent.");
1139
+ if (!message.body && !ids.length) throw new Error(error || t("The file could not be sent."));
989
1140
  const sent = await api().sendChat({
990
1141
  body: message.body,
1142
+ ...item.draft !== null ? { conversation: "new" } : item.chat ? { conversation: item.chat } : {},
991
1143
  email: given,
992
1144
  url: window.location.href.slice(0, 500),
993
1145
  ...ids.length ? { attachments: ids } : {},
@@ -996,6 +1148,18 @@ var build = (o) => {
996
1148
  });
997
1149
  ctrl?.abort();
998
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
+ }
999
1163
  take(sent);
1000
1164
  outbox.shift();
1001
1165
  for (const p of files) {
@@ -1004,10 +1168,12 @@ var build = (o) => {
1004
1168
  }
1005
1169
  } catch (e) {
1006
1170
  const back = outbox.splice(0);
1171
+ const gone = e.status === 404;
1172
+ enterChat(gone ? null : back[0].chat, gone ? ++drafts : back[0].draft);
1007
1173
  text.value = [...back.map((b) => b.message.body), text.value.trim()].filter(Boolean).join("\n");
1008
1174
  picked = [...back.flatMap((b) => b.files.filter((p) => p.id)), ...picked];
1009
1175
  for (const b of back) for (const p of b.files) localUrls.delete(p.key);
1010
- error = sayable(e);
1176
+ error = gone ? t("This conversation is no longer available. Your message is still here to send as a new one.") : sayable(e);
1011
1177
  }
1012
1178
  draw();
1013
1179
  }
@@ -1029,7 +1195,9 @@ var build = (o) => {
1029
1195
  ...files.length ? { attachments: files.map((p) => ({ id: p.key, name: p.name, type: p.type, size: p.size })) } : {}
1030
1196
  },
1031
1197
  files,
1032
- email: needEmail() && email.value.trim() ? email.value.trim() : void 0
1198
+ email: needEmail() && email.value.trim() ? email.value.trim() : void 0,
1199
+ chat: chatId,
1200
+ draft
1033
1201
  });
1034
1202
  text.value = "";
1035
1203
  picked = [];
@@ -1074,7 +1242,7 @@ var build = (o) => {
1074
1242
  function show(v = "home", still = false) {
1075
1243
  const was = open;
1076
1244
  open = true;
1077
- view = v === "help" && !helpOn ? "home" : v;
1245
+ view = v === "help" && !helpOn || v === "updates" && !updatesOn ? "home" : v;
1078
1246
  drawn = -1;
1079
1247
  previewGone = true;
1080
1248
  pop.toggleAttribute("data-still", still);
@@ -1115,10 +1283,14 @@ var build = (o) => {
1115
1283
  document.addEventListener("keydown", onKey);
1116
1284
  void tick();
1117
1285
  if (helpOn) void loadArticles();
1286
+ if (updatesOn) void loadUpdates();
1118
1287
  drawFab();
1119
1288
  if (o.opened) show(o.opened, true);
1120
1289
  return {
1121
- open: (v) => show(v ?? "home"),
1290
+ open: (v) => {
1291
+ if (v === "messages") enterMessages();
1292
+ show(v ?? "home");
1293
+ },
1122
1294
  close,
1123
1295
  toggle: () => open ? close() : show(),
1124
1296
  isOpen: () => open,
@@ -1127,7 +1299,7 @@ var build = (o) => {
1127
1299
  },
1128
1300
  destroy: () => {
1129
1301
  clearTimeout(poll);
1130
- for (const t of leaving.values()) clearTimeout(t);
1302
+ for (const t2 of leaving.values()) clearTimeout(t2);
1131
1303
  ctrl?.abort();
1132
1304
  for (const p of picked) forget(p);
1133
1305
  for (const u of localUrls.values()) URL.revokeObjectURL(u);