@mrrocket/sdk 0.8.0 → 0.9.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.
- package/CHANGELOG.md +13 -0
- package/README.md +35 -1
- package/dist/api-client.d.ts +213 -0
- package/dist/api-client.js +67 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api.d.ts +2 -122
- package/dist/api.js +2 -61
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-messenger.js +313 -0
- package/dist/browser/mrrocket-replay.js +3 -3
- package/dist/browser/mrrocket.js +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -13
- package/dist/capture.js +1 -22
- package/dist/capture.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/feedback.js +5 -2
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/messenger-ui.d.ts +47 -0
- package/dist/messenger-ui.js +583 -0
- package/dist/messenger-ui.js.map +1 -0
- package/dist/messenger.d.ts +37 -0
- package/dist/messenger.js +69 -0
- package/dist/messenger.js.map +1 -0
- package/dist/rage.d.ts +14 -0
- package/dist/rage.js +27 -0
- package/dist/rage.js.map +1 -0
- package/dist/react-native/index.d.cts +339 -4
- package/dist/react-native/index.d.ts +339 -4
- package/dist/react-native/index.js +1191 -31
- package/dist/react-native/index.js.map +1 -1
- package/dist/styles-css.d.ts +6 -0
- package/dist/styles-css.js +327 -0
- package/dist/styles-css.js.map +1 -0
- package/dist/styles.d.ts +1 -2
- package/dist/styles.js +3 -237
- package/dist/styles.js.map +1 -1
- package/package.json +13 -5
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
// src/messenger-ui.ts
|
|
2
|
+
import { createApi } from "./api-client.js";
|
|
3
|
+
import { injectStyles } from "./styles-css.js";
|
|
4
|
+
var POLL_OPEN_MS = 4e3;
|
|
5
|
+
var POLL_WAITING_MS = 8e3;
|
|
6
|
+
var POLL_IDLE_MS = 3e4;
|
|
7
|
+
var WAITING_FOR_MS = 10 * 6e4;
|
|
8
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
9
|
+
var SEEN_KEY = "ckw_msgr_seen";
|
|
10
|
+
var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
|
|
11
|
+
var readSeen = () => {
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage.getItem(SEEN_KEY) ?? "";
|
|
14
|
+
} catch {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var writeSeen = (id) => {
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(SEEN_KEY, id);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var timeAgo = (iso) => {
|
|
25
|
+
const t = new Date(iso).getTime();
|
|
26
|
+
if (Number.isNaN(t)) return "";
|
|
27
|
+
const d = Math.max(0, Date.now() - t) / 1e3;
|
|
28
|
+
if (d < 60) return "just now";
|
|
29
|
+
if (d < 3600) return `${Math.floor(d / 60)}m ago`;
|
|
30
|
+
if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
|
|
31
|
+
if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;
|
|
32
|
+
return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
33
|
+
};
|
|
34
|
+
var el = (tag, attrs = {}, ...kids) => {
|
|
35
|
+
const e = document.createElement(tag);
|
|
36
|
+
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
|
|
37
|
+
for (const k of kids) if (k !== null && k !== false && k !== void 0) e.append(k);
|
|
38
|
+
return e;
|
|
39
|
+
};
|
|
40
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
41
|
+
var icon = (d, size = 16) => {
|
|
42
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
43
|
+
for (const [k, v] of Object.entries({ class: "ckw-icon", width: String(size), height: String(size), viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", "stroke-width": "1.7", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }))
|
|
44
|
+
svg.setAttribute(k, v);
|
|
45
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
46
|
+
path.setAttribute("d", d);
|
|
47
|
+
svg.appendChild(path);
|
|
48
|
+
return svg;
|
|
49
|
+
};
|
|
50
|
+
var I = {
|
|
51
|
+
cross: "m4 4 8 8M12 4l-8 8",
|
|
52
|
+
chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
|
|
53
|
+
send: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5",
|
|
54
|
+
home: "M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z",
|
|
55
|
+
help: "M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1",
|
|
56
|
+
back: "M10 3.5 5.5 8l4.5 4.5",
|
|
57
|
+
next: "M6 3.5 10.5 8 6 12.5",
|
|
58
|
+
search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
|
|
59
|
+
doc: "M4 1.5h5.5l3 3V14a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5zM9.5 1.5v3h3M6 8.5h4M6 11h4"
|
|
60
|
+
};
|
|
61
|
+
var chime = /* @__PURE__ */ (() => {
|
|
62
|
+
let ctx = null;
|
|
63
|
+
const get = () => {
|
|
64
|
+
const AC = window.AudioContext ?? window.webkitAudioContext;
|
|
65
|
+
if (!AC) return null;
|
|
66
|
+
ctx ??= new AC();
|
|
67
|
+
return ctx;
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
/** Call from a click so later sounds may play. */
|
|
71
|
+
unlock: () => {
|
|
72
|
+
try {
|
|
73
|
+
const c = get();
|
|
74
|
+
if (c?.state === "suspended") void c.resume();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
play: () => {
|
|
79
|
+
try {
|
|
80
|
+
const c = get();
|
|
81
|
+
if (!c || c.state !== "running") return;
|
|
82
|
+
const t = c.currentTime;
|
|
83
|
+
[880, 1318.5].forEach((f, i) => {
|
|
84
|
+
const o = c.createOscillator();
|
|
85
|
+
const g = c.createGain();
|
|
86
|
+
const at = t + i * 0.11;
|
|
87
|
+
o.type = "sine";
|
|
88
|
+
o.frequency.value = f;
|
|
89
|
+
g.gain.setValueAtTime(1e-4, at);
|
|
90
|
+
g.gain.exponentialRampToValueAtTime(0.12, at + 0.012);
|
|
91
|
+
g.gain.exponentialRampToValueAtTime(1e-4, at + 0.32);
|
|
92
|
+
o.connect(g);
|
|
93
|
+
g.connect(c.destination);
|
|
94
|
+
o.start(at);
|
|
95
|
+
o.stop(at + 0.34);
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
})();
|
|
102
|
+
var spansOf = (spans) => spans.map((s) => {
|
|
103
|
+
let node = s.href ? el("a", { href: s.href, target: "_blank", rel: "noopener noreferrer" }, s.text) : el("span", {}, s.text);
|
|
104
|
+
if (s.code) node = el("code", {}, node);
|
|
105
|
+
if (s.italic) node = el("em", {}, node);
|
|
106
|
+
if (s.bold) node = el("strong", {}, node);
|
|
107
|
+
return node;
|
|
108
|
+
});
|
|
109
|
+
var blockOf = (b) => {
|
|
110
|
+
switch (b.type) {
|
|
111
|
+
case "h2":
|
|
112
|
+
case "h3":
|
|
113
|
+
return el(b.type, {}, ...spansOf(b.spans));
|
|
114
|
+
case "quote":
|
|
115
|
+
return el("blockquote", {}, ...spansOf(b.spans));
|
|
116
|
+
case "ul":
|
|
117
|
+
case "ol":
|
|
118
|
+
return el(b.type, {}, ...b.items.map((it) => el("li", {}, ...spansOf(it))));
|
|
119
|
+
case "code":
|
|
120
|
+
return el("pre", {}, el("code", {}, b.text));
|
|
121
|
+
case "hr":
|
|
122
|
+
return el("hr");
|
|
123
|
+
default:
|
|
124
|
+
return el("p", {}, ...spansOf(b.spans));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var LOOK_WAIT_MS = 2500;
|
|
128
|
+
var mountMessenger = (o) => {
|
|
129
|
+
let real = null;
|
|
130
|
+
let gone = false;
|
|
131
|
+
const queued = [];
|
|
132
|
+
const later = (fn) => real ? fn(real) : queued.push(fn);
|
|
133
|
+
const ctrl = new AbortController();
|
|
134
|
+
const start = (look) => {
|
|
135
|
+
if (real || gone) return;
|
|
136
|
+
const defined = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
137
|
+
real = build({ ...look ?? {}, ...defined, accent: o.accent ?? look?.accent, theme: o.theme ?? look?.theme });
|
|
138
|
+
for (const fn of queued.splice(0)) fn(real);
|
|
139
|
+
};
|
|
140
|
+
const timer = setTimeout(() => start(null), LOOK_WAIT_MS);
|
|
141
|
+
createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() }).messengerLook(ctrl.signal).then((look) => start(look)).catch(() => start(null)).finally(() => clearTimeout(timer));
|
|
142
|
+
return {
|
|
143
|
+
open: (v) => later((h) => h.open(v)),
|
|
144
|
+
close: () => real?.close(),
|
|
145
|
+
toggle: () => later((h) => h.toggle()),
|
|
146
|
+
destroy: () => {
|
|
147
|
+
gone = true;
|
|
148
|
+
ctrl.abort();
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
real?.destroy();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
var build = (o) => {
|
|
155
|
+
injectStyles();
|
|
156
|
+
const position = o.position ?? "bottom-right";
|
|
157
|
+
const title = o.title || "Chat with us";
|
|
158
|
+
const greeting = o.greeting || "Hi! How can we help?";
|
|
159
|
+
const askEmail = o.askEmail ?? true;
|
|
160
|
+
const label = o.label ?? "";
|
|
161
|
+
const launcherOn = o.launcher ?? true;
|
|
162
|
+
const helpOn = o.help ?? true;
|
|
163
|
+
const soundOn = o.sound ?? true;
|
|
164
|
+
const previewOn = o.preview ?? true;
|
|
165
|
+
const api = () => createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() });
|
|
166
|
+
const root = el("div", { class: "ckw-root", "data-theme": o.theme ?? "auto" });
|
|
167
|
+
if (o.accent) root.style.setProperty("--ckw-accent", o.accent);
|
|
168
|
+
document.body.appendChild(root);
|
|
169
|
+
let open = false;
|
|
170
|
+
let view = "home";
|
|
171
|
+
let thread = null;
|
|
172
|
+
let loaded = false;
|
|
173
|
+
let error = "";
|
|
174
|
+
let pending = null;
|
|
175
|
+
let lastKnown = null;
|
|
176
|
+
let unseen = [];
|
|
177
|
+
let previewGone = false;
|
|
178
|
+
let poll;
|
|
179
|
+
let ctrl = null;
|
|
180
|
+
let articles = null;
|
|
181
|
+
let query = "";
|
|
182
|
+
let rated = /* @__PURE__ */ new Set();
|
|
183
|
+
let hasArticles = null;
|
|
184
|
+
const teamMsgs = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
|
|
185
|
+
const lastTeamId = (t) => teamMsgs(t).at(-1)?.id ?? "";
|
|
186
|
+
const watching = () => open && view === "messages" && document.visibilityState !== "hidden";
|
|
187
|
+
const take = (t) => {
|
|
188
|
+
const team = teamMsgs(t);
|
|
189
|
+
if (lastKnown === null) {
|
|
190
|
+
const seen = readSeen();
|
|
191
|
+
const i = team.findIndex((m) => m.id === seen);
|
|
192
|
+
unseen = t.messages.some((m) => m.from === "visitor") ? team.slice(i + 1).filter((m) => m.from !== "auto") : [];
|
|
193
|
+
} else {
|
|
194
|
+
const i = team.findIndex((m) => m.id === lastKnown);
|
|
195
|
+
const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
|
|
196
|
+
if (fresh.length) {
|
|
197
|
+
if (soundOn) chime.play();
|
|
198
|
+
if (!watching()) {
|
|
199
|
+
unseen = [...unseen, ...fresh];
|
|
200
|
+
previewGone = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lastKnown = lastTeamId(t);
|
|
205
|
+
thread = t;
|
|
206
|
+
if (watching()) markSeen();
|
|
207
|
+
};
|
|
208
|
+
const markSeen = () => {
|
|
209
|
+
unseen = [];
|
|
210
|
+
const id = lastTeamId(thread);
|
|
211
|
+
if (id) writeSeen(id);
|
|
212
|
+
};
|
|
213
|
+
const fab = el("button", { type: "button", class: `ckw-fab ckw-msgr-fab${label ? "" : " ckw-msgr-round"}`, "data-pos": position, "aria-haspopup": "dialog" });
|
|
214
|
+
const prev = el("div", { class: "ckw-msgr-preview", "data-pos": position, role: "status" });
|
|
215
|
+
const drawFab = () => {
|
|
216
|
+
fab.replaceChildren(icon(open ? I.cross : I.chat, label ? 16 : 20));
|
|
217
|
+
if (label) fab.append(el("span", {}, label));
|
|
218
|
+
if (unseen.length && !open) fab.append(el("span", { class: "ckw-msgr-count", "aria-hidden": "true" }, String(Math.min(unseen.length, 9))));
|
|
219
|
+
fab.setAttribute("aria-expanded", String(open));
|
|
220
|
+
fab.setAttribute("aria-label", open ? "Close chat" : unseen.length ? `Open chat, ${unseen.length} new ${unseen.length === 1 ? "reply" : "replies"}` : "Open chat");
|
|
221
|
+
drawPreview();
|
|
222
|
+
};
|
|
223
|
+
const drawPreview = () => {
|
|
224
|
+
const m = unseen.at(-1);
|
|
225
|
+
if (!previewOn || open || previewGone || !m || !launcherOn) return prev.remove();
|
|
226
|
+
const x = el("button", { type: "button", class: "ckw-msgr-preview-x", "aria-label": "Dismiss" }, icon(I.cross, 12));
|
|
227
|
+
x.addEventListener("click", (e) => {
|
|
228
|
+
e.stopPropagation();
|
|
229
|
+
previewGone = true;
|
|
230
|
+
drawPreview();
|
|
231
|
+
});
|
|
232
|
+
const card = el(
|
|
233
|
+
"button",
|
|
234
|
+
{ type: "button", class: "ckw-msgr-preview-card" },
|
|
235
|
+
el("span", { class: "ckw-msgr-preview-who" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` \xB7 ${unseen.length} new` : ""}`),
|
|
236
|
+
el("span", { class: "ckw-msgr-preview-text" }, m.body)
|
|
237
|
+
);
|
|
238
|
+
card.addEventListener("click", () => show("messages"));
|
|
239
|
+
prev.replaceChildren(card, x);
|
|
240
|
+
if (!prev.isConnected) root.appendChild(prev);
|
|
241
|
+
};
|
|
242
|
+
fab.addEventListener("click", () => {
|
|
243
|
+
chime.unlock();
|
|
244
|
+
if (open) close();
|
|
245
|
+
else show(unseen.length ? "messages" : "home");
|
|
246
|
+
});
|
|
247
|
+
if (launcherOn) root.appendChild(fab);
|
|
248
|
+
const pop = el("div", { class: "ckw-pop ckw-msgr", "data-pos": position, role: "dialog", "aria-label": title });
|
|
249
|
+
const head = el("div", { class: "ckw-pop-head ckw-msgr-head" });
|
|
250
|
+
const body = el("div", { class: "ckw-pop-body ckw-msgr-body" });
|
|
251
|
+
const tabs = el("nav", { class: "ckw-msgr-tabs", "aria-label": "Messenger" });
|
|
252
|
+
pop.addEventListener("click", () => chime.unlock());
|
|
253
|
+
const form = el("form", { class: "ckw-msgr-compose" });
|
|
254
|
+
const err = el("p", { class: "ckw-msgr-error", role: "alert" });
|
|
255
|
+
const email = el("input", { class: "ckw-input", type: "email", inputmode: "email", autocomplete: "email", placeholder: "Your email (optional), so we can follow up", "aria-label": "Your email (optional)", maxlength: "200" });
|
|
256
|
+
const row = el("div", { class: "ckw-msgr-row" });
|
|
257
|
+
const text = el("textarea", { class: "ckw-textarea ckw-msgr-input", rows: "1", placeholder: "Write a message\u2026", "aria-label": "Message", maxlength: "4000" });
|
|
258
|
+
const send = el("button", { type: "submit", class: "ckw-btn ckw-btn-primary ckw-msgr-send", "aria-label": "Send" }, icon(I.send));
|
|
259
|
+
row.append(text, send);
|
|
260
|
+
form.append(row);
|
|
261
|
+
const list = el("ol", { class: "ckw-msgs", "aria-live": "polite", "aria-label": "Messages" });
|
|
262
|
+
const needEmail = () => askEmail && !o.user()?.email && !thread?.hasEmail;
|
|
263
|
+
const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());
|
|
264
|
+
const canSend = () => !!text.value.trim() && !pending && emailOk();
|
|
265
|
+
const closeBtn = () => {
|
|
266
|
+
const b = el("button", { type: "button", class: "ckw-btn ckw-btn-ghost ckw-msgr-x", "aria-label": "Close messenger" }, icon(I.cross));
|
|
267
|
+
b.addEventListener("click", () => close());
|
|
268
|
+
return b;
|
|
269
|
+
};
|
|
270
|
+
const go = (v) => {
|
|
271
|
+
view = v;
|
|
272
|
+
drawn = -1;
|
|
273
|
+
draw();
|
|
274
|
+
if (v === "messages") {
|
|
275
|
+
markSeen();
|
|
276
|
+
drawFab();
|
|
277
|
+
text.focus();
|
|
278
|
+
} else if (v === "help") helpInput.focus();
|
|
279
|
+
schedule(0);
|
|
280
|
+
};
|
|
281
|
+
const bubble = (m, isPending = false) => {
|
|
282
|
+
const li = el("li", { class: "ckw-msg", "data-from": m.from === "visitor" ? "me" : "them" }, el("div", { class: "ckw-bubble" }, m.body));
|
|
283
|
+
if (isPending) li.setAttribute("aria-busy", "true");
|
|
284
|
+
for (const a of m.articles ?? []) {
|
|
285
|
+
const chip = el("button", { type: "button", class: "ckw-msgr-cite" }, icon(I.doc, 14), el("span", {}, a.title));
|
|
286
|
+
chip.addEventListener("click", () => go({ article: a.id }));
|
|
287
|
+
li.append(chip);
|
|
288
|
+
}
|
|
289
|
+
li.append(el("span", { class: "ckw-msg-meta" }, `${m.from !== "visitor" ? `${whoLabel[m.from]} \xB7 ` : ""}${isPending ? "Sending\u2026" : timeAgo(m.at)}`));
|
|
290
|
+
return li;
|
|
291
|
+
};
|
|
292
|
+
const articleRow = (a) => {
|
|
293
|
+
const b = el("button", { type: "button", class: "ckw-msgr-art" }, el("span", { class: "ckw-msgr-art-t" }, a.title), icon(I.next, 14));
|
|
294
|
+
b.addEventListener("click", () => go({ article: a.id }));
|
|
295
|
+
return b;
|
|
296
|
+
};
|
|
297
|
+
const searchBox = () => {
|
|
298
|
+
const wrap = el("label", { class: "ckw-msgr-search" }, icon(I.search, 14));
|
|
299
|
+
const input = el("input", { class: "ckw-msgr-search-in", type: "search", placeholder: "Search for help", "aria-label": "Search for help", maxlength: "200" });
|
|
300
|
+
input.value = query;
|
|
301
|
+
let t;
|
|
302
|
+
input.addEventListener("input", () => {
|
|
303
|
+
query = input.value;
|
|
304
|
+
clearTimeout(t);
|
|
305
|
+
t = setTimeout(() => {
|
|
306
|
+
if (view !== "help") return go("help");
|
|
307
|
+
void loadArticles(true);
|
|
308
|
+
}, 250);
|
|
309
|
+
});
|
|
310
|
+
input.addEventListener("keydown", (e) => {
|
|
311
|
+
if (e.key === "Enter") {
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
if (view !== "help") go("help");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
wrap.append(input);
|
|
317
|
+
return wrap;
|
|
318
|
+
};
|
|
319
|
+
const helpSearch = searchBox();
|
|
320
|
+
const helpInput = helpSearch.querySelector("input");
|
|
321
|
+
let loadingArticles = false;
|
|
322
|
+
const loadArticles = async (force = false) => {
|
|
323
|
+
if (!helpOn || loadingArticles && !force) return;
|
|
324
|
+
loadingArticles = true;
|
|
325
|
+
const q = query.trim();
|
|
326
|
+
try {
|
|
327
|
+
const r = await api().articles(q || void 0);
|
|
328
|
+
if (q !== query.trim()) return;
|
|
329
|
+
articles = r.articles;
|
|
330
|
+
if (!q) hasArticles = r.articles.length > 0;
|
|
331
|
+
} catch {
|
|
332
|
+
articles = articles ?? [];
|
|
333
|
+
} finally {
|
|
334
|
+
loadingArticles = false;
|
|
335
|
+
}
|
|
336
|
+
draw();
|
|
337
|
+
};
|
|
338
|
+
const drawHome = () => {
|
|
339
|
+
head.replaceChildren(el("span", { class: "ckw-msgr-brand" }, title), closeBtn());
|
|
340
|
+
const hero = el("div", { class: "ckw-msgr-hero" }, el("p", { class: "ckw-msgr-hello" }, greeting));
|
|
341
|
+
const last = (thread?.messages ?? []).at(-1);
|
|
342
|
+
const msgCard = el(
|
|
343
|
+
"button",
|
|
344
|
+
{ type: "button", class: "ckw-msgr-card" },
|
|
345
|
+
el(
|
|
346
|
+
"span",
|
|
347
|
+
{ class: "ckw-msgr-card-main" },
|
|
348
|
+
el("b", {}, last ? "Recent message" : "Send us a message"),
|
|
349
|
+
el("span", { class: "ckw-msgr-card-sub" }, last ? `${last.from === "visitor" ? "You" : whoLabel[last.from]}: ${last.body}` : "We usually answer within a few hours")
|
|
350
|
+
),
|
|
351
|
+
unseen.length ? el("span", { class: "ckw-msgr-badge" }, String(unseen.length)) : icon(I.send)
|
|
352
|
+
);
|
|
353
|
+
msgCard.addEventListener("click", () => go("messages"));
|
|
354
|
+
const parts = [hero, msgCard];
|
|
355
|
+
if (helpOn && hasArticles !== false) {
|
|
356
|
+
const helpCard = el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, searchBox());
|
|
357
|
+
const top = (articles ?? []).slice(0, 4);
|
|
358
|
+
if (top.length) helpCard.append(...top.map(articleRow));
|
|
359
|
+
else if (articles === null) helpCard.append(el("div", { class: "ckw-skel ckw-msgr-skel" }));
|
|
360
|
+
parts.push(helpCard);
|
|
361
|
+
}
|
|
362
|
+
body.replaceChildren(...parts);
|
|
363
|
+
};
|
|
364
|
+
const drawHelp = () => {
|
|
365
|
+
head.replaceChildren(el("span", {}, "Help"), closeBtn());
|
|
366
|
+
if (helpInput.value !== query) helpInput.value = query;
|
|
367
|
+
const parts = [helpSearch];
|
|
368
|
+
if (articles === null) parts.push(el("div", { class: "ckw-skel" }));
|
|
369
|
+
else if (!articles.length) {
|
|
370
|
+
const ask = el("button", { type: "button", class: "ckw-btn ckw-btn-primary" }, "Send us a message");
|
|
371
|
+
ask.addEventListener("click", () => go("messages"));
|
|
372
|
+
parts.push(el("div", { class: "ckw-msgr-empty" }, el("p", {}, query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), ask));
|
|
373
|
+
} else if (query.trim()) parts.push(el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, ...articles.map(articleRow)));
|
|
374
|
+
else {
|
|
375
|
+
const groups = /* @__PURE__ */ new Map();
|
|
376
|
+
for (const a of articles) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
|
|
377
|
+
for (const [name, items] of groups)
|
|
378
|
+
parts.push(el("section", { class: "ckw-msgr-group" }, el("h3", { class: "ckw-msgr-group-h" }, `${name} \xB7 ${items.length}`), el("div", { class: "ckw-msgr-card ckw-msgr-card-col" }, ...items.map(articleRow))));
|
|
379
|
+
}
|
|
380
|
+
if (body.firstChild !== helpSearch) body.replaceChildren(helpSearch);
|
|
381
|
+
while (body.childNodes.length > 1) body.lastChild.remove();
|
|
382
|
+
body.append(...parts.slice(1));
|
|
383
|
+
};
|
|
384
|
+
let articleShown = null;
|
|
385
|
+
const drawArticle = (id) => {
|
|
386
|
+
const back = el("button", { type: "button", class: "ckw-btn ckw-btn-ghost ckw-msgr-x", "aria-label": "Back to help" }, icon(I.back));
|
|
387
|
+
back.addEventListener("click", () => go("help"));
|
|
388
|
+
head.replaceChildren(back, el("span", { class: "ckw-msgr-head-t" }, "Help"), closeBtn());
|
|
389
|
+
if (articleShown?.id === id) return body.replaceChildren(articleShown.node);
|
|
390
|
+
const node = el("article", { class: "ckw-msgr-doc" }, el("div", { class: "ckw-skel" }));
|
|
391
|
+
articleShown = { id, node };
|
|
392
|
+
body.replaceChildren(node);
|
|
393
|
+
api().article(id).then((a) => {
|
|
394
|
+
const foot = el("div", { class: "ckw-msgr-rate" });
|
|
395
|
+
const drawRate = () => {
|
|
396
|
+
if (rated.has(id)) return foot.replaceChildren(el("span", {}, "Thanks for letting us know."));
|
|
397
|
+
const yes = el("button", { type: "button", class: "ckw-btn" }, "Yes");
|
|
398
|
+
const no = el("button", { type: "button", class: "ckw-btn" }, "No");
|
|
399
|
+
const rate = (helpful) => {
|
|
400
|
+
rated = new Set(rated).add(id);
|
|
401
|
+
drawRate();
|
|
402
|
+
api().rateArticle(id, helpful).catch(() => void 0);
|
|
403
|
+
if (!helpful) go("messages");
|
|
404
|
+
};
|
|
405
|
+
yes.addEventListener("click", () => rate(true));
|
|
406
|
+
no.addEventListener("click", () => rate(false));
|
|
407
|
+
foot.replaceChildren(el("span", {}, "Did this answer your question?"), el("span", { class: "ckw-msgr-rate-b" }, yes, no));
|
|
408
|
+
};
|
|
409
|
+
drawRate();
|
|
410
|
+
const ask = el("button", { type: "button", class: "ckw-btn ckw-msgr-ask" }, icon(I.chat), "Still need help? Send us a message");
|
|
411
|
+
ask.addEventListener("click", () => go("messages"));
|
|
412
|
+
node.replaceChildren(
|
|
413
|
+
el("p", { class: "ckw-msgr-doc-c" }, a.collection),
|
|
414
|
+
el("h2", { class: "ckw-msgr-doc-t" }, a.title),
|
|
415
|
+
el("p", { class: "ckw-msgr-doc-c" }, `Updated ${timeAgo(a.updatedAt)}`),
|
|
416
|
+
el("div", { class: "ckw-msgr-doc-b" }, ...a.blocks.map(blockOf)),
|
|
417
|
+
foot,
|
|
418
|
+
ask
|
|
419
|
+
);
|
|
420
|
+
}).catch((e) => {
|
|
421
|
+
articleShown = null;
|
|
422
|
+
node.replaceChildren(el("p", { class: "ckw-msgr-error" }, e.message));
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
let drawn = -1;
|
|
426
|
+
let listSig = "";
|
|
427
|
+
const drawMessages = () => {
|
|
428
|
+
head.replaceChildren(el("span", {}, title), closeBtn());
|
|
429
|
+
const msgs = thread?.messages ?? [];
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (!loaded) parts.push(el("div", { class: "ckw-skel" }));
|
|
432
|
+
else {
|
|
433
|
+
const sig = `${msgs.map((m) => m.id).join(",")}|${pending ? pending.body : ""}|${Math.floor(Date.now() / 6e4)}`;
|
|
434
|
+
if (sig !== listSig) {
|
|
435
|
+
listSig = sig;
|
|
436
|
+
const greet = el("li", { class: "ckw-msg", "data-from": "them" }, el("div", { class: "ckw-bubble" }, greeting));
|
|
437
|
+
list.replaceChildren(greet, ...msgs.map((m) => bubble(m)), ...pending ? [bubble(pending, true)] : []);
|
|
438
|
+
}
|
|
439
|
+
parts.push(list);
|
|
440
|
+
}
|
|
441
|
+
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."));
|
|
442
|
+
if (body.childNodes.length !== parts.length || parts.some((x, i) => body.childNodes[i] !== x)) body.replaceChildren(...parts);
|
|
443
|
+
const count = msgs.length + (pending ? 1 : 0);
|
|
444
|
+
if (count !== drawn) {
|
|
445
|
+
drawn = count;
|
|
446
|
+
body.scrollTop = body.scrollHeight;
|
|
447
|
+
}
|
|
448
|
+
err.textContent = error;
|
|
449
|
+
if (error && !err.isConnected) form.prepend(err);
|
|
450
|
+
else if (!error) err.remove();
|
|
451
|
+
if (needEmail()) {
|
|
452
|
+
if (emailOk()) email.removeAttribute("aria-invalid");
|
|
453
|
+
else email.setAttribute("aria-invalid", "true");
|
|
454
|
+
if (!email.isConnected) form.insertBefore(email, row);
|
|
455
|
+
} else email.remove();
|
|
456
|
+
send.disabled = !canSend();
|
|
457
|
+
};
|
|
458
|
+
const drawTabs = () => {
|
|
459
|
+
const current = typeof view === "string" ? view : "help";
|
|
460
|
+
const tab = (v, name, d, badge = 0) => {
|
|
461
|
+
const b = el("button", { type: "button", class: "ckw-msgr-tab", "aria-current": current === v ? "page" : "false" }, icon(d, 18), el("span", {}, name));
|
|
462
|
+
if (badge) b.append(el("span", { class: "ckw-msgr-count ckw-msgr-count-tab" }, String(Math.min(badge, 9))));
|
|
463
|
+
b.addEventListener("click", () => go(v));
|
|
464
|
+
return b;
|
|
465
|
+
};
|
|
466
|
+
tabs.replaceChildren(tab("home", "Home", I.home), tab("messages", "Messages", I.chat, view === "messages" ? 0 : unseen.length), ...helpOn && hasArticles !== false ? [tab("help", "Help", I.help)] : []);
|
|
467
|
+
};
|
|
468
|
+
const draw = () => {
|
|
469
|
+
if (!open) return;
|
|
470
|
+
pop.dataset.view = typeof view === "string" ? view : "article";
|
|
471
|
+
if (view === "home") drawHome();
|
|
472
|
+
else if (view === "messages") drawMessages();
|
|
473
|
+
else if (view === "help") drawHelp();
|
|
474
|
+
else drawArticle(view.article);
|
|
475
|
+
const kids = [head, body];
|
|
476
|
+
if (view === "messages") kids.push(form);
|
|
477
|
+
kids.push(tabs);
|
|
478
|
+
drawTabs();
|
|
479
|
+
if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);
|
|
480
|
+
};
|
|
481
|
+
const tick = async () => {
|
|
482
|
+
ctrl?.abort();
|
|
483
|
+
ctrl = new AbortController();
|
|
484
|
+
try {
|
|
485
|
+
take(await api().chat(ctrl.signal));
|
|
486
|
+
error = "";
|
|
487
|
+
} catch (e) {
|
|
488
|
+
if (e.name === "AbortError") return;
|
|
489
|
+
if (open && view === "messages") error = e.message;
|
|
490
|
+
}
|
|
491
|
+
loaded = true;
|
|
492
|
+
draw();
|
|
493
|
+
drawFab();
|
|
494
|
+
schedule();
|
|
495
|
+
};
|
|
496
|
+
function schedule(now) {
|
|
497
|
+
clearTimeout(poll);
|
|
498
|
+
const waiting = !!thread?.messages.some((m) => m.from === "visitor") && thread?.conversation?.status !== "solved";
|
|
499
|
+
const wrote = (thread?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
|
|
500
|
+
const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
|
|
501
|
+
const ms = now ?? (watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : open || waiting ? POLL_IDLE_MS : 0);
|
|
502
|
+
if (ms || now === 0) poll = setTimeout(() => void tick(), ms);
|
|
503
|
+
}
|
|
504
|
+
document.addEventListener("visibilitychange", () => {
|
|
505
|
+
if (document.visibilityState === "visible") schedule(0);
|
|
506
|
+
else clearTimeout(poll);
|
|
507
|
+
});
|
|
508
|
+
const submit = async () => {
|
|
509
|
+
const msg = text.value.trim();
|
|
510
|
+
if (!msg || pending || !emailOk()) return;
|
|
511
|
+
chime.unlock();
|
|
512
|
+
pending = { id: "pending", from: "visitor", body: msg, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
513
|
+
text.value = "";
|
|
514
|
+
error = "";
|
|
515
|
+
draw();
|
|
516
|
+
try {
|
|
517
|
+
take(await api().sendChat({ body: msg, email: needEmail() && email.value.trim() ? email.value.trim() : void 0, url: window.location.href.slice(0, 500) }));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
text.value = msg;
|
|
520
|
+
error = e.message;
|
|
521
|
+
}
|
|
522
|
+
pending = null;
|
|
523
|
+
draw();
|
|
524
|
+
schedule();
|
|
525
|
+
};
|
|
526
|
+
form.addEventListener("submit", (e) => {
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
void submit();
|
|
529
|
+
});
|
|
530
|
+
text.addEventListener("keydown", (e) => {
|
|
531
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
void submit();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
text.addEventListener("input", () => send.disabled = !canSend());
|
|
537
|
+
email.addEventListener("input", () => draw());
|
|
538
|
+
function show(v = "home") {
|
|
539
|
+
open = true;
|
|
540
|
+
view = v === "help" && !helpOn ? "home" : v;
|
|
541
|
+
drawn = -1;
|
|
542
|
+
previewGone = true;
|
|
543
|
+
if (!pop.isConnected) root.appendChild(pop);
|
|
544
|
+
if (articles === null || view === "help" && query) void loadArticles();
|
|
545
|
+
draw();
|
|
546
|
+
drawFab();
|
|
547
|
+
if (view === "messages") {
|
|
548
|
+
markSeen();
|
|
549
|
+
text.focus();
|
|
550
|
+
}
|
|
551
|
+
schedule(0);
|
|
552
|
+
}
|
|
553
|
+
function close() {
|
|
554
|
+
if (!open) return;
|
|
555
|
+
open = false;
|
|
556
|
+
pop.remove();
|
|
557
|
+
drawFab();
|
|
558
|
+
schedule();
|
|
559
|
+
if (launcherOn) fab.focus();
|
|
560
|
+
}
|
|
561
|
+
const onKey = (e) => {
|
|
562
|
+
if (e.key === "Escape" && open) close();
|
|
563
|
+
};
|
|
564
|
+
document.addEventListener("keydown", onKey);
|
|
565
|
+
void tick();
|
|
566
|
+
if (helpOn) void loadArticles();
|
|
567
|
+
drawFab();
|
|
568
|
+
return {
|
|
569
|
+
open: (v) => show(v ?? "home"),
|
|
570
|
+
close,
|
|
571
|
+
toggle: () => open ? close() : show(),
|
|
572
|
+
destroy: () => {
|
|
573
|
+
clearTimeout(poll);
|
|
574
|
+
ctrl?.abort();
|
|
575
|
+
document.removeEventListener("keydown", onKey);
|
|
576
|
+
root.remove();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
export {
|
|
581
|
+
mountMessenger
|
|
582
|
+
};
|
|
583
|
+
//# sourceMappingURL=messenger-ui.js.map
|