@guidelytech/guide-runtime 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/dist/capture.cjs +8 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.js +2 -2
- package/dist/chunk-KJRXCITD.js +515 -0
- package/dist/chunk-KJRXCITD.js.map +1 -0
- package/dist/chunk-PG7CMBHD.js +330 -0
- package/dist/chunk-PG7CMBHD.js.map +1 -0
- package/dist/chunk-VNJPNGZ4.js +301 -0
- package/dist/chunk-VNJPNGZ4.js.map +1 -0
- package/dist/index.cjs +138 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +74 -10
- package/dist/index.js.map +1 -1
- package/dist/messenger.cjs +586 -35
- package/dist/messenger.cjs.map +1 -1
- package/dist/messenger.d.cts +21 -1
- package/dist/messenger.d.ts +21 -1
- package/dist/messenger.js +527 -23
- package/dist/messenger.js.map +1 -1
- package/package.json +5 -5
package/dist/messenger.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createHelpView,
|
|
3
3
|
el,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
resultRow,
|
|
5
|
+
startableGuide
|
|
6
|
+
} from "./chunk-KJRXCITD.js";
|
|
7
|
+
import "./chunk-PG7CMBHD.js";
|
|
7
8
|
|
|
8
9
|
// src/messenger.ts
|
|
9
|
-
import { foldPathIds, pageLabel } from "@guidelytech/shared/runtime";
|
|
10
|
+
import { foldPathIds, matchesHelpQuery, pageLabel } from "@guidelytech/shared/runtime";
|
|
10
11
|
|
|
11
12
|
// src/howto-view.ts
|
|
12
13
|
var pct = (v) => `${(v * 100).toFixed(3)}%`;
|
|
@@ -67,6 +68,15 @@ function renderHowTo(doc, howTo, onShowMe) {
|
|
|
67
68
|
show.addEventListener("click", () => onShowMe(guideKey));
|
|
68
69
|
wrap.appendChild(show);
|
|
69
70
|
}
|
|
71
|
+
if (howTo.video && /^https?:\/\//i.test(howTo.video.url)) {
|
|
72
|
+
const video = el(doc, "video", "g-howto-video");
|
|
73
|
+
video.src = howTo.video.url;
|
|
74
|
+
video.controls = true;
|
|
75
|
+
video.preload = "metadata";
|
|
76
|
+
video.playsInline = true;
|
|
77
|
+
video.setAttribute("aria-label", `Video: ${howTo.title}`);
|
|
78
|
+
wrap.appendChild(video);
|
|
79
|
+
}
|
|
70
80
|
const list = el(doc, "ol", "g-howto-steps");
|
|
71
81
|
let n = 0;
|
|
72
82
|
for (const s of howTo.steps) {
|
|
@@ -100,6 +110,275 @@ function renderHowTo(doc, howTo, onShowMe) {
|
|
|
100
110
|
return wrap;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
// src/complaint-view.ts
|
|
114
|
+
import { MAX_COMPLAINT_ANSWER_LENGTH, MAX_COMPLAINT_ATTACHMENT_BYTES, MAX_COMPLAINT_LENGTH } from "@guidelytech/shared/runtime";
|
|
115
|
+
var emptyComplaintDraft = (description = "") => ({ description, answers: {}, email: "", attachment: null, error: null, sending: false, checked: false });
|
|
116
|
+
var COMPLAINT_STEPS = [
|
|
117
|
+
["open", "Received"],
|
|
118
|
+
["in_review", "In review"],
|
|
119
|
+
["resolved", "Resolved"]
|
|
120
|
+
];
|
|
121
|
+
var complaintStatusLabel = (s) => COMPLAINT_STEPS.find(([k]) => k === s)?.[1] ?? s;
|
|
122
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
123
|
+
function complaintProblem(category, draft, needsEmail) {
|
|
124
|
+
for (const f of category.fields) if (f.required && !(draft.answers[f.id] ?? "").trim()) return `${f.label} is required.`;
|
|
125
|
+
if (!draft.description.trim()) return "Tell us what happened.";
|
|
126
|
+
if (needsEmail && !EMAIL.test(draft.email.trim())) return "Enter your email address so the team can reply.";
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function complaintAnswers(category, draft) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const f of category.fields) {
|
|
132
|
+
const v = (draft.answers[f.id] ?? "").trim();
|
|
133
|
+
if (v) out[f.id] = v.slice(0, MAX_COMPLAINT_ANSWER_LENGTH);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
function statusPill(doc, status) {
|
|
138
|
+
return el(doc, "span", `g-m-pill g-m-pill-${status}`, complaintStatusLabel(status));
|
|
139
|
+
}
|
|
140
|
+
function dot(doc, color) {
|
|
141
|
+
const d = el(doc, "span", "g-m-cat-dot");
|
|
142
|
+
if (/^#[0-9a-fA-F]{6}$/.test(color)) d.style.background = color;
|
|
143
|
+
d.setAttribute("aria-hidden", "true");
|
|
144
|
+
return d;
|
|
145
|
+
}
|
|
146
|
+
function categoryList(doc, categories, onPick) {
|
|
147
|
+
const wrap = el(doc, "div", "g-m-form");
|
|
148
|
+
const intro = el(doc, "div");
|
|
149
|
+
intro.append(el(doc, "h3", "g-m-form-title", "What's it about?"), el(doc, "p", "g-m-card-sub", "Picking the right one gets it to the right team faster."));
|
|
150
|
+
wrap.appendChild(intro);
|
|
151
|
+
const list = el(doc, "div", "g-m-cats");
|
|
152
|
+
list.setAttribute("role", "list");
|
|
153
|
+
for (const c of categories) {
|
|
154
|
+
const b = el(doc, "button", "g-m-card g-m-row g-m-center g-m-cat");
|
|
155
|
+
b.type = "button";
|
|
156
|
+
b.setAttribute("role", "listitem");
|
|
157
|
+
b.appendChild(dot(doc, c.color));
|
|
158
|
+
const text = el(doc, "span", "g-m-grow");
|
|
159
|
+
text.appendChild(el(doc, "span", "g-m-card-title", c.name));
|
|
160
|
+
if (c.description) text.appendChild(el(doc, "span", "g-m-card-sub", c.description));
|
|
161
|
+
b.appendChild(text);
|
|
162
|
+
b.addEventListener("click", () => onPick(c));
|
|
163
|
+
list.appendChild(b);
|
|
164
|
+
}
|
|
165
|
+
wrap.appendChild(list);
|
|
166
|
+
return wrap;
|
|
167
|
+
}
|
|
168
|
+
var fieldSeq = 0;
|
|
169
|
+
function field(doc, label, control, opts = {}) {
|
|
170
|
+
const id = `g-m-f${++fieldSeq}`;
|
|
171
|
+
control.id = id;
|
|
172
|
+
const wrap = el(doc, "div", "g-m-field");
|
|
173
|
+
const l = el(doc, "label", "g-m-label", label);
|
|
174
|
+
l.htmlFor = id;
|
|
175
|
+
if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
|
|
176
|
+
wrap.append(l, control);
|
|
177
|
+
if (opts.hint) {
|
|
178
|
+
const hint = el(doc, "span", "g-m-hint", opts.hint);
|
|
179
|
+
hint.id = `${id}-hint`;
|
|
180
|
+
control.setAttribute("aria-describedby", hint.id);
|
|
181
|
+
wrap.appendChild(hint);
|
|
182
|
+
}
|
|
183
|
+
if (opts.invalid) control.setAttribute("aria-invalid", "true");
|
|
184
|
+
return wrap;
|
|
185
|
+
}
|
|
186
|
+
function complaintForm(doc, o) {
|
|
187
|
+
const { category, draft } = o;
|
|
188
|
+
const form = el(doc, "form", "g-m-form");
|
|
189
|
+
form.noValidate = true;
|
|
190
|
+
const picked = el(doc, "div", "g-m-picked g-m-row g-m-center");
|
|
191
|
+
picked.append(dot(doc, category.color), el(doc, "span", "g-m-grow g-m-card-title", category.name));
|
|
192
|
+
if (o.canChangeCategory) {
|
|
193
|
+
const change = el(doc, "button", "g-m-text-btn", "Change");
|
|
194
|
+
change.type = "button";
|
|
195
|
+
change.setAttribute("aria-label", `Change category (${category.name})`);
|
|
196
|
+
change.addEventListener("click", o.onChangeCategory);
|
|
197
|
+
picked.appendChild(change);
|
|
198
|
+
}
|
|
199
|
+
form.appendChild(picked);
|
|
200
|
+
for (const f of category.fields) {
|
|
201
|
+
const missing = draft.checked && f.required && !(draft.answers[f.id] ?? "").trim();
|
|
202
|
+
let control;
|
|
203
|
+
if (f.type === "choice") {
|
|
204
|
+
const select = el(doc, "select", "g-input");
|
|
205
|
+
const option = (label, value) => {
|
|
206
|
+
const o2 = el(doc, "option", "", label);
|
|
207
|
+
o2.value = value;
|
|
208
|
+
return o2;
|
|
209
|
+
};
|
|
210
|
+
select.appendChild(option(f.required ? "Choose one" : "Choose one (optional)", ""));
|
|
211
|
+
for (const opt of f.options ?? []) select.appendChild(option(opt, opt));
|
|
212
|
+
select.value = draft.answers[f.id] ?? "";
|
|
213
|
+
select.addEventListener("change", () => draft.answers[f.id] = select.value);
|
|
214
|
+
control = select;
|
|
215
|
+
} else {
|
|
216
|
+
const input = el(doc, "input", "g-input");
|
|
217
|
+
input.type = "text";
|
|
218
|
+
input.maxLength = MAX_COMPLAINT_ANSWER_LENGTH;
|
|
219
|
+
input.value = draft.answers[f.id] ?? "";
|
|
220
|
+
input.addEventListener("input", () => draft.answers[f.id] = input.value);
|
|
221
|
+
control = input;
|
|
222
|
+
}
|
|
223
|
+
if (f.required) control.required = true;
|
|
224
|
+
form.appendChild(field(doc, f.label, control, { optional: !f.required, invalid: missing }));
|
|
225
|
+
}
|
|
226
|
+
const what = el(doc, "textarea", "g-input g-m-textarea");
|
|
227
|
+
what.rows = 5;
|
|
228
|
+
what.maxLength = MAX_COMPLAINT_LENGTH;
|
|
229
|
+
what.required = true;
|
|
230
|
+
what.value = draft.description;
|
|
231
|
+
what.placeholder = "What went wrong, and when?";
|
|
232
|
+
what.addEventListener("input", () => draft.description = what.value);
|
|
233
|
+
form.appendChild(field(doc, "What happened?", what, { invalid: draft.checked && !draft.description.trim() }));
|
|
234
|
+
if (o.needsEmail) {
|
|
235
|
+
const email = el(doc, "input", "g-input");
|
|
236
|
+
email.type = "email";
|
|
237
|
+
email.autocomplete = "email";
|
|
238
|
+
email.required = true;
|
|
239
|
+
email.value = draft.email;
|
|
240
|
+
email.addEventListener("input", () => draft.email = email.value);
|
|
241
|
+
form.appendChild(field(doc, "Your email", email, { hint: "So the team can reply to you.", invalid: draft.checked && !EMAIL.test(draft.email.trim()) }));
|
|
242
|
+
}
|
|
243
|
+
const files = el(doc, "div", "g-m-attach");
|
|
244
|
+
if (draft.attachment) {
|
|
245
|
+
const a = draft.attachment;
|
|
246
|
+
const row = el(doc, "div", "g-m-row g-m-center g-m-attached");
|
|
247
|
+
if (a.previewUrl) {
|
|
248
|
+
const img = el(doc, "img", "g-m-thumb");
|
|
249
|
+
img.src = a.previewUrl;
|
|
250
|
+
img.alt = "";
|
|
251
|
+
img.addEventListener("error", () => img.remove());
|
|
252
|
+
row.appendChild(img);
|
|
253
|
+
}
|
|
254
|
+
row.appendChild(el(doc, "span", "g-m-grow g-m-ellipsis", a.fileName));
|
|
255
|
+
const remove = el(doc, "button", "g-m-text-btn", "Remove");
|
|
256
|
+
remove.type = "button";
|
|
257
|
+
remove.setAttribute("aria-label", `Remove ${a.fileName}`);
|
|
258
|
+
remove.addEventListener("click", o.onRemoveFile);
|
|
259
|
+
row.appendChild(remove);
|
|
260
|
+
files.appendChild(row);
|
|
261
|
+
} else {
|
|
262
|
+
const input = el(doc, "input", "g-m-file");
|
|
263
|
+
input.type = "file";
|
|
264
|
+
input.accept = "image/png,image/jpeg,image/webp,image/gif";
|
|
265
|
+
input.tabIndex = -1;
|
|
266
|
+
input.setAttribute("aria-hidden", "true");
|
|
267
|
+
input.addEventListener("change", () => {
|
|
268
|
+
const file = input.files?.[0];
|
|
269
|
+
if (file) o.onPickFile(file);
|
|
270
|
+
});
|
|
271
|
+
const pick = el(doc, "button", "g-m-attach-btn", "Attach a screenshot");
|
|
272
|
+
pick.type = "button";
|
|
273
|
+
pick.addEventListener("click", () => input.click());
|
|
274
|
+
files.append(input, pick);
|
|
275
|
+
}
|
|
276
|
+
form.appendChild(files);
|
|
277
|
+
const foot = el(doc, "div", "g-m-form-foot");
|
|
278
|
+
foot.appendChild(el(doc, "p", "g-m-hint", "We'll include the page you're on so the team can look into it."));
|
|
279
|
+
if (draft.error) {
|
|
280
|
+
const err = el(doc, "p", "g-m-error", draft.error);
|
|
281
|
+
err.setAttribute("role", "alert");
|
|
282
|
+
foot.appendChild(err);
|
|
283
|
+
}
|
|
284
|
+
const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit complaint");
|
|
285
|
+
submit.type = "submit";
|
|
286
|
+
submit.disabled = draft.sending;
|
|
287
|
+
foot.appendChild(submit);
|
|
288
|
+
form.appendChild(foot);
|
|
289
|
+
form.addEventListener("submit", (e) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
if (!draft.sending) o.onSubmit();
|
|
292
|
+
});
|
|
293
|
+
return form;
|
|
294
|
+
}
|
|
295
|
+
function statusTracker(doc, status) {
|
|
296
|
+
const list = el(doc, "ol", "g-m-steps");
|
|
297
|
+
list.setAttribute("aria-label", "Progress");
|
|
298
|
+
const at = COMPLAINT_STEPS.findIndex(([k]) => k === status);
|
|
299
|
+
COMPLAINT_STEPS.forEach(([, label], i) => {
|
|
300
|
+
const li = el(doc, "li", `g-m-step${i <= at ? " g-m-step-done" : ""}`);
|
|
301
|
+
if (i === at) li.setAttribute("aria-current", "step");
|
|
302
|
+
li.append(el(doc, "span", "g-m-step-dot"), el(doc, "span", "g-m-step-label", label));
|
|
303
|
+
list.appendChild(li);
|
|
304
|
+
});
|
|
305
|
+
return list;
|
|
306
|
+
}
|
|
307
|
+
function complaintSent(doc, o) {
|
|
308
|
+
const wrap = el(doc, "div", "g-m-form g-m-sent");
|
|
309
|
+
const top = el(doc, "div", "g-m-sent-top");
|
|
310
|
+
const tick = el(doc, "span", "g-m-sent-tick");
|
|
311
|
+
tick.setAttribute("aria-hidden", "true");
|
|
312
|
+
tick.textContent = "\u2713";
|
|
313
|
+
const p = el(doc, "p", "g-m-card-sub");
|
|
314
|
+
p.append("Your reference is ", el(doc, "strong", "", o.complaint.reference), ". The team will reply in this conversation, so check back here.");
|
|
315
|
+
top.append(tick, el(doc, "h3", "g-m-form-title", "We've got your complaint"), p);
|
|
316
|
+
wrap.appendChild(top);
|
|
317
|
+
const details = el(doc, "dl", "g-m-card g-m-static g-m-details");
|
|
318
|
+
const row = (k, v) => {
|
|
319
|
+
const r = el(doc, "div", "g-m-detail");
|
|
320
|
+
r.append(el(doc, "dt", "", k), el(doc, "dd", "", v));
|
|
321
|
+
details.appendChild(r);
|
|
322
|
+
};
|
|
323
|
+
row("Category", o.complaint.categoryName);
|
|
324
|
+
o.answers.slice(0, 3).forEach(([k, v]) => row(k, v));
|
|
325
|
+
wrap.appendChild(details);
|
|
326
|
+
wrap.appendChild(statusTracker(doc, o.complaint.status));
|
|
327
|
+
const open = el(doc, "button", "g-btn g-btn-primary g-m-wide", "Add more detail");
|
|
328
|
+
open.type = "button";
|
|
329
|
+
open.addEventListener("click", o.onOpenConversation);
|
|
330
|
+
const home = el(doc, "button", "g-btn g-btn-secondary g-m-wide", "Back to home");
|
|
331
|
+
home.type = "button";
|
|
332
|
+
home.addEventListener("click", o.onHome);
|
|
333
|
+
wrap.append(open, home);
|
|
334
|
+
return wrap;
|
|
335
|
+
}
|
|
336
|
+
var MAX_SIDE = 1600;
|
|
337
|
+
async function readScreenshot(doc, file) {
|
|
338
|
+
if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error("Attach a PNG, JPEG, WebP or GIF image.");
|
|
339
|
+
if (file.size > 25 * 1024 * 1024) throw new Error("That image is too large.");
|
|
340
|
+
const win = doc.defaultView;
|
|
341
|
+
const asDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
342
|
+
const r = new FileReader();
|
|
343
|
+
r.onload = () => resolve(String(r.result));
|
|
344
|
+
r.onerror = () => reject(new Error("The image couldn't be read."));
|
|
345
|
+
r.readAsDataURL(blob);
|
|
346
|
+
});
|
|
347
|
+
let dataUrl = null;
|
|
348
|
+
let scaled = false;
|
|
349
|
+
try {
|
|
350
|
+
const url = win?.URL.createObjectURL(file);
|
|
351
|
+
if (!url) throw new Error("no URL");
|
|
352
|
+
const img = await new Promise((resolve, reject) => {
|
|
353
|
+
const i = new Image();
|
|
354
|
+
i.onload = () => resolve(i);
|
|
355
|
+
i.onerror = () => reject(new Error("decode"));
|
|
356
|
+
i.src = url;
|
|
357
|
+
}).finally(() => win?.URL.revokeObjectURL(url));
|
|
358
|
+
const scale = Math.min(1, MAX_SIDE / Math.max(img.naturalWidth || 1, img.naturalHeight || 1));
|
|
359
|
+
const canvas = doc.createElement("canvas");
|
|
360
|
+
canvas.width = Math.max(1, Math.round(img.naturalWidth * scale));
|
|
361
|
+
canvas.height = Math.max(1, Math.round(img.naturalHeight * scale));
|
|
362
|
+
const ctx = canvas.getContext("2d");
|
|
363
|
+
if (!ctx) throw new Error("no canvas");
|
|
364
|
+
ctx.fillStyle = "#fff";
|
|
365
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
366
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
367
|
+
for (const q of [0.85, 0.7, 0.5]) {
|
|
368
|
+
dataUrl = canvas.toDataURL("image/jpeg", q);
|
|
369
|
+
if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= MAX_COMPLAINT_ATTACHMENT_BYTES) break;
|
|
370
|
+
}
|
|
371
|
+
scaled = true;
|
|
372
|
+
} catch {
|
|
373
|
+
dataUrl = file.size <= MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
|
|
374
|
+
}
|
|
375
|
+
if (!dataUrl) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
|
|
376
|
+
const data = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
|
377
|
+
if (data.length * 0.75 > MAX_COMPLAINT_ATTACHMENT_BYTES) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
|
|
378
|
+
const name = (file.name || "screenshot").slice(0, 180);
|
|
379
|
+
return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
|
|
380
|
+
}
|
|
381
|
+
|
|
103
382
|
// src/messenger-styles.ts
|
|
104
383
|
var MESSENGER_STYLES = `
|
|
105
384
|
:host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
|
|
@@ -109,7 +388,15 @@ var MESSENGER_STYLES = `
|
|
|
109
388
|
.g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
|
|
110
389
|
.g-m-launcher svg{width:26px;height:26px;fill:currentColor}
|
|
111
390
|
.g-m-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;padding:0 5px;border-radius:999px;background:#e11d48;color:#fff;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;border:2px solid var(--g-bg)}
|
|
112
|
-
.g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px))}
|
|
391
|
+
.g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px));transition:width .2s ease,height .2s ease,max-height .2s ease}
|
|
392
|
+
.g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - 110px);max-height:calc(100vh - 110px)}
|
|
393
|
+
.g-m-expanded .g-rc-article,.g-m-expanded .g-m-scroll{padding:24px clamp(20px,5vw,40px)}
|
|
394
|
+
.g-m-expanded .g-rc-article>.g-title{font-size:24px;line-height:1.25;margin-bottom:12px}
|
|
395
|
+
.g-m-expanded .g-rc-article .g-body{font-size:15px}
|
|
396
|
+
.g-m-head.g-m-sizable{padding-right:92px}
|
|
397
|
+
.g-m-size{position:absolute;right:48px;top:50%;transform:translateY(-50%);width:40px;height:40px;border:0;border-radius:999px;background:transparent;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
|
398
|
+
.g-m-size .g-m-ico{width:18px;height:18px}
|
|
399
|
+
.g-m-size:hover,.g-m-size:focus-visible{background:rgba(255,255,255,.18);outline:none}
|
|
113
400
|
.g-m-head{padding:16px 48px 14px 20px;background:var(--g-primary);color:var(--g-primary-text);position:relative;display:flex;align-items:center;gap:8px;min-height:56px}
|
|
114
401
|
.g-m-head .g-close{color:inherit;top:50%;transform:translateY(-50%)}
|
|
115
402
|
.g-m-head .g-close:hover,.g-m-head .g-close:focus-visible{background:rgba(255,255,255,.18);color:inherit}
|
|
@@ -179,6 +466,7 @@ var MESSENGER_STYLES = `
|
|
|
179
466
|
.g-howto-desc,.g-howto-note{color:var(--g-muted);font-size:13px}
|
|
180
467
|
.g-howto p{margin:0 0 6px;white-space:pre-line}
|
|
181
468
|
.g-howto-show{width:100%;margin:6px 0 4px}
|
|
469
|
+
.g-howto-video{display:block;width:100%;max-height:260px;margin:8px 0 4px;border-radius:8px;background:#000}
|
|
182
470
|
.g-howto-steps{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:16px}
|
|
183
471
|
.g-howto-step h4{margin:4px 0 0;font-size:15px}
|
|
184
472
|
.g-howto-tip,.g-howto-alert{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px;font-size:13px}
|
|
@@ -248,6 +536,52 @@ var MESSENGER_STYLES = `
|
|
|
248
536
|
.g-m-asks{align-items:flex-start}
|
|
249
537
|
.g-m-chip.g-m-ask{border-color:var(--g-primary);color:var(--g-primary);font-weight:600;border-radius:999px;padding:8px 14px;min-height:36px}
|
|
250
538
|
.g-m-chip.g-m-ask:disabled{opacity:.5;cursor:not-allowed}
|
|
539
|
+
.g-m-side{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex:none}
|
|
540
|
+
.g-m-pill{display:inline-block;font-size:11px;font-weight:700;line-height:1.5;padding:1px 8px;border-radius:999px;white-space:nowrap;background:#fef3c7;color:#92400e}
|
|
541
|
+
.g-m-pill-in_review{background:#e0e7ff;color:#3730a3}
|
|
542
|
+
.g-m-pill-resolved{background:#dcfce7;color:#166534}
|
|
543
|
+
.g-m-complain .g-m-badge-ico{background:#fef3e2;color:#b45309}
|
|
544
|
+
.g-m-complaint{align-self:stretch;gap:10px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:10px 12px}
|
|
545
|
+
.g-m-chip-ico{width:16px;height:16px}
|
|
546
|
+
.g-m-offer{gap:6px}
|
|
547
|
+
.g-m-form{display:flex;flex-direction:column;gap:14px;margin:0}
|
|
548
|
+
.g-m-form-title{margin:0;font-size:17px;font-weight:600}
|
|
549
|
+
.g-m-cats{display:flex;flex-direction:column;gap:10px}
|
|
550
|
+
.g-m-cats .g-m-card{margin:0;gap:12px;min-height:44px}
|
|
551
|
+
.g-m-cat-dot{width:10px;height:10px;border-radius:999px;background:var(--g-muted);flex:none}
|
|
552
|
+
.g-m-picked{gap:8px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:8px 12px}
|
|
553
|
+
.g-m-text-btn{border:0;background:transparent;color:var(--g-primary);font:inherit;font-size:13px;font-weight:600;cursor:pointer;padding:6px 4px;min-height:32px}
|
|
554
|
+
.g-m-text-btn:hover{text-decoration:underline}
|
|
555
|
+
.g-m-text-btn:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px;border-radius:4px}
|
|
556
|
+
.g-m-field{display:flex;flex-direction:column;gap:4px}
|
|
557
|
+
.g-m-label{font-weight:600;font-size:13px}
|
|
558
|
+
.g-m-optional{font-weight:400;color:var(--g-muted)}
|
|
559
|
+
.g-m-hint{color:var(--g-muted);font-size:12px;margin:0}
|
|
560
|
+
.g-m-error{color:#b91c1c;font-size:13px;margin:0}
|
|
561
|
+
.g-scope[data-theme="dark"] .g-m-error{color:#fca5a5}
|
|
562
|
+
.g-m-field .g-input[aria-invalid="true"]{border-color:#dc2626}
|
|
563
|
+
.g-m-textarea{min-height:110px}
|
|
564
|
+
.g-m-file{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
|
|
565
|
+
.g-m-attach-btn{align-self:flex-start;border:1px dashed var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:13px;border-radius:8px;padding:8px 12px;cursor:pointer;min-height:36px}
|
|
566
|
+
.g-m-attach-btn:hover{background:var(--g-subtle)}
|
|
567
|
+
.g-m-attach-btn:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
|
|
568
|
+
.g-m-attached{gap:10px;border:1px solid var(--g-border);border-radius:8px;padding:6px 8px;font-size:13px}
|
|
569
|
+
.g-m-thumb{width:40px;height:40px;object-fit:cover;border-radius:6px;background:var(--g-subtle);flex:none}
|
|
570
|
+
.g-m-form-foot{display:flex;flex-direction:column;gap:10px;margin-top:4px}
|
|
571
|
+
.g-m-sent{gap:12px}
|
|
572
|
+
.g-m-sent-top{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding-top:8px}
|
|
573
|
+
.g-m-sent-top p{max-width:300px;margin:0}
|
|
574
|
+
.g-m-sent-tick{width:52px;height:52px;border-radius:999px;background:#dcfce7;color:#15803d;display:inline-flex;align-items:center;justify-content:center;font-size:26px;font-weight:700}
|
|
575
|
+
.g-m-details{margin:0;display:flex;flex-direction:column;gap:8px}
|
|
576
|
+
.g-m-detail{display:flex;justify-content:space-between;gap:12px;font-size:13px}
|
|
577
|
+
.g-m-detail dt{color:var(--g-muted)}
|
|
578
|
+
.g-m-detail dd{margin:0;font-weight:600;text-align:right;overflow-wrap:anywhere}
|
|
579
|
+
.g-m-steps{list-style:none;margin:4px 0;padding:0;display:flex;justify-content:space-between;position:relative}
|
|
580
|
+
.g-m-steps::before{content:"";position:absolute;top:6px;left:12%;right:12%;height:2px;background:var(--g-border)}
|
|
581
|
+
.g-m-step{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;font-size:12px;color:var(--g-muted)}
|
|
582
|
+
.g-m-step-dot{width:14px;height:14px;border-radius:999px;border:2px solid var(--g-border);background:var(--g-bg);box-sizing:border-box}
|
|
583
|
+
.g-m-step-done{color:var(--g-text);font-weight:600}
|
|
584
|
+
.g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
|
|
251
585
|
`;
|
|
252
586
|
|
|
253
587
|
// src/render-safe.ts
|
|
@@ -353,7 +687,10 @@ var ICONS = {
|
|
|
353
687
|
article: ["M6 3h9l4 4v14H6z", "M9 12h6", "M9 16h6"],
|
|
354
688
|
ask: ["M4 5h16v11H8l-4 4z", "M9 9.5h6", "M9 12.5h4"],
|
|
355
689
|
search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "m20 20-3.5-3.5"],
|
|
356
|
-
circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"]
|
|
690
|
+
circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
|
|
691
|
+
expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
|
|
692
|
+
flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
|
|
693
|
+
collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
|
|
357
694
|
};
|
|
358
695
|
function icon(doc, name, cls = "g-m-ico") {
|
|
359
696
|
const svg = doc.createElementNS(SVG_NS, "svg");
|
|
@@ -432,6 +769,7 @@ function renderMessenger(options) {
|
|
|
432
769
|
let current = null;
|
|
433
770
|
const streaming = /* @__PURE__ */ new Map();
|
|
434
771
|
let sending = false;
|
|
772
|
+
let complaintDraft = emptyComplaintDraft();
|
|
435
773
|
let draft = "";
|
|
436
774
|
let articles = null;
|
|
437
775
|
let articlesLoading = false;
|
|
@@ -506,8 +844,38 @@ function renderMessenger(options) {
|
|
|
506
844
|
options.onOpenChecklist?.(id);
|
|
507
845
|
},
|
|
508
846
|
onArticleView: options.onArticleView,
|
|
509
|
-
extra: (q) => howToItems(q)
|
|
847
|
+
extra: (q) => howToItems(q),
|
|
848
|
+
guides: () => options.guides ?? [],
|
|
849
|
+
onShowMe: (key) => startWalkthrough(key),
|
|
850
|
+
onArticleChange: () => syncSize()
|
|
510
851
|
});
|
|
852
|
+
let docCollapsed = false;
|
|
853
|
+
let sizeButton = null;
|
|
854
|
+
const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
|
|
855
|
+
function syncSize() {
|
|
856
|
+
const reading = readingDoc();
|
|
857
|
+
if (!reading) docCollapsed = false;
|
|
858
|
+
panel.classList.toggle("g-m-expanded", reading && !docCollapsed);
|
|
859
|
+
head.classList.toggle("g-m-sizable", reading);
|
|
860
|
+
if (!reading) {
|
|
861
|
+
sizeButton?.remove();
|
|
862
|
+
sizeButton = null;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (!sizeButton) {
|
|
866
|
+
sizeButton = el(doc, "button", "g-m-size");
|
|
867
|
+
sizeButton.type = "button";
|
|
868
|
+
sizeButton.addEventListener("click", () => {
|
|
869
|
+
docCollapsed = !docCollapsed;
|
|
870
|
+
syncSize();
|
|
871
|
+
});
|
|
872
|
+
head.appendChild(sizeButton);
|
|
873
|
+
}
|
|
874
|
+
sizeButton.textContent = "";
|
|
875
|
+
sizeButton.appendChild(icon(doc, docCollapsed ? "expand" : "collapse"));
|
|
876
|
+
sizeButton.setAttribute("aria-label", docCollapsed ? "Expand" : "Collapse");
|
|
877
|
+
sizeButton.title = docCollapsed ? "Expand" : "Collapse";
|
|
878
|
+
}
|
|
511
879
|
const unreadTotal = () => conversations.reduce((n, c) => n + (c.unread > 0 ? 1 : 0), 0);
|
|
512
880
|
const updateBadge = () => {
|
|
513
881
|
const n = unreadTotal();
|
|
@@ -515,7 +883,13 @@ function renderMessenger(options) {
|
|
|
515
883
|
badge.textContent = n > 9 ? "9+" : String(n);
|
|
516
884
|
launcher.setAttribute("aria-label", n ? `Open ${config.name}, ${n} unread` : `Open ${config.name}`);
|
|
517
885
|
};
|
|
518
|
-
const
|
|
886
|
+
const withComplaint = (c) => {
|
|
887
|
+
if (c.complaint) return c;
|
|
888
|
+
const known = conversations.find((x) => x.id === c.id)?.complaint ?? (current?.id === c.id ? current.complaint : void 0);
|
|
889
|
+
return known ? { ...c, complaint: known } : c;
|
|
890
|
+
};
|
|
891
|
+
const upsertConversation = (incoming) => {
|
|
892
|
+
const c = withComplaint(incoming);
|
|
519
893
|
conversations = [c, ...conversations.filter((x) => x.id !== c.id)].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
|
|
520
894
|
if (current?.id === c.id) current = c;
|
|
521
895
|
updateBadge();
|
|
@@ -550,15 +924,19 @@ function renderMessenger(options) {
|
|
|
550
924
|
const pageHowTos = () => (howTos?.forPage ?? []).map((id) => howTos?.howTos.find((h) => h.id === id)).filter((h) => !!h);
|
|
551
925
|
const stepsLabel = (h) => `${h.stepCount} ${h.stepCount === 1 ? "step" : "steps"}`;
|
|
552
926
|
const openHowTo = (id) => go({ kind: "howto", id, from: view });
|
|
927
|
+
const startWalkthrough = (guideKey) => {
|
|
928
|
+
setOpen(false);
|
|
929
|
+
options.onStartGuide?.(guideKey);
|
|
930
|
+
};
|
|
553
931
|
function howToItems(q) {
|
|
554
|
-
const list = q ? (howTos?.howTos ?? []).filter((h) => h.title
|
|
932
|
+
const list = q ? (howTos?.howTos ?? []).filter((h) => matchesHelpQuery(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
|
|
555
933
|
if (!list.length) return [];
|
|
556
934
|
return [
|
|
557
935
|
el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
|
|
558
936
|
...list.map((h) => {
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
return
|
|
937
|
+
const key = h.guideKey && startableGuide(options.guides, h.guideKey) ? h.guideKey : null;
|
|
938
|
+
const sub = [h.hasVideo ? "Video" : "", stepsLabel(h)].filter(Boolean).join(" \xB7 ");
|
|
939
|
+
return resultRow(doc, "\u{1F4CB}", h.title, h.description ? `${sub} \xB7 ${h.description}` : sub, () => openHowTo(h.id), key ? () => startWalkthrough(key) : void 0);
|
|
562
940
|
})
|
|
563
941
|
];
|
|
564
942
|
}
|
|
@@ -570,6 +948,7 @@ function renderMessenger(options) {
|
|
|
570
948
|
});
|
|
571
949
|
function renderHead(title, back) {
|
|
572
950
|
head.textContent = "";
|
|
951
|
+
sizeButton = null;
|
|
573
952
|
head.hidden = false;
|
|
574
953
|
if (back) {
|
|
575
954
|
const b = el(doc, "button", "g-m-back");
|
|
@@ -609,9 +988,9 @@ function renderMessenger(options) {
|
|
|
609
988
|
i.appendChild(icon(doc, ico));
|
|
610
989
|
b.append(i, el(doc, "span", "", label));
|
|
611
990
|
if (tab === "news" && latest.some((n) => n.unread)) {
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
i.appendChild(
|
|
991
|
+
const dot2 = el(doc, "span", "g-m-tab-dot");
|
|
992
|
+
dot2.setAttribute("aria-label", "New");
|
|
993
|
+
i.appendChild(dot2);
|
|
615
994
|
}
|
|
616
995
|
b.addEventListener("click", () => go({ kind: "tab", tab }));
|
|
617
996
|
tabs.appendChild(b);
|
|
@@ -625,7 +1004,10 @@ function renderMessenger(options) {
|
|
|
625
1004
|
const text = el(doc, "div", "g-m-grow");
|
|
626
1005
|
if (label) text.appendChild(el(doc, "div", "g-m-card-label", label));
|
|
627
1006
|
text.append(el(doc, "div", "g-m-card-title g-m-ellipsis", c.subject || "Conversation"), el(doc, "div", "g-m-card-sub g-m-ellipsis", c.lastMessagePreview));
|
|
628
|
-
|
|
1007
|
+
const side = el(doc, "span", "g-m-side");
|
|
1008
|
+
side.appendChild(el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
|
|
1009
|
+
if (c.complaint) side.appendChild(statusPill(doc, c.complaint.status));
|
|
1010
|
+
row.append(text, side);
|
|
629
1011
|
card.appendChild(row);
|
|
630
1012
|
card.setAttribute("aria-label", `${label ? `${label}: ` : ""}${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
|
|
631
1013
|
card.addEventListener("click", () => go({ kind: "thread", conversationId: c.id }));
|
|
@@ -836,7 +1218,10 @@ function renderMessenger(options) {
|
|
|
836
1218
|
hello.append(el(doc, "span", "g-m-hello-1", v.firstName ? `Hi ${v.firstName}.` : "Hi there."), el(doc, "span", "g-m-hello-2", "How can we help?"));
|
|
837
1219
|
inner.append(top, hello);
|
|
838
1220
|
inner.appendChild(actionCard({ title: "Send us a message", sub: messageSub(), trail: true, onClick: () => go({ kind: "thread", conversationId: null }) }));
|
|
839
|
-
|
|
1221
|
+
if (canComplain()) inner.appendChild(actionCard({ title: "File a complaint", sub: "Tell us what went wrong. You'll get a reference.", lead: iconTile("flag"), onClick: () => startComplaint(null), cls: "g-m-complain" }));
|
|
1222
|
+
const filed = latestComplaint();
|
|
1223
|
+
if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
|
|
1224
|
+
const recent = conversations.find((c) => c.id !== filed?.id);
|
|
840
1225
|
if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
|
|
841
1226
|
if (config.status) inner.appendChild(statusCard(config.status));
|
|
842
1227
|
if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
|
|
@@ -861,6 +1246,102 @@ function renderMessenger(options) {
|
|
|
861
1246
|
scroll.append(hero, inner);
|
|
862
1247
|
main.appendChild(scroll);
|
|
863
1248
|
}
|
|
1249
|
+
const complaintCategories = () => config.complaints?.categories ?? [];
|
|
1250
|
+
const needsEmail = () => visitor().identified === false;
|
|
1251
|
+
const canComplain = () => !!api.fileComplaint && complaintCategories().length > 0 && (!needsEmail() || !!config.complaints?.anonymous);
|
|
1252
|
+
function latestComplaint() {
|
|
1253
|
+
let best = null;
|
|
1254
|
+
for (const c of conversations) if (c.complaint && (!best || c.complaint.createdAt > best.complaint.createdAt)) best = c;
|
|
1255
|
+
return best;
|
|
1256
|
+
}
|
|
1257
|
+
function userMessagesText() {
|
|
1258
|
+
return messages.filter((m) => m.author === "user" && m.body.trim()).slice(-3).map((m) => m.body.trim()).join("\n\n").slice(0, 4e3);
|
|
1259
|
+
}
|
|
1260
|
+
function startComplaint(categoryId) {
|
|
1261
|
+
const cats = complaintCategories();
|
|
1262
|
+
const picked = categoryId ? cats.find((c) => c.id === categoryId) : cats.length === 1 ? cats[0] : void 0;
|
|
1263
|
+
const from = view.kind === "tab" || view.kind === "thread" ? view : { kind: "tab", tab: "home" };
|
|
1264
|
+
complaintDraft = emptyComplaintDraft(complaintDraft.description);
|
|
1265
|
+
if (picked) go({ kind: "complaint-form", categoryId: picked.id, conversationId: null, from });
|
|
1266
|
+
else go({ kind: "complaint-category", conversationId: null, from });
|
|
1267
|
+
}
|
|
1268
|
+
function complaintBanner(c) {
|
|
1269
|
+
const b = el(doc, "div", "g-m-complaint g-m-row g-m-center");
|
|
1270
|
+
b.setAttribute("role", "status");
|
|
1271
|
+
b.appendChild(icon(doc, "flag", "g-m-ico g-m-lead"));
|
|
1272
|
+
const text = el(doc, "span", "g-m-grow");
|
|
1273
|
+
text.append(el(doc, "span", "g-m-card-title", c.reference), el(doc, "span", "g-m-card-sub", c.categoryName));
|
|
1274
|
+
b.append(text, statusPill(doc, c.status));
|
|
1275
|
+
return b;
|
|
1276
|
+
}
|
|
1277
|
+
function renderComplaintCategories(v) {
|
|
1278
|
+
renderHead("File a complaint", () => go(v.from));
|
|
1279
|
+
const scroll = el(doc, "div", "g-m-scroll");
|
|
1280
|
+
scroll.appendChild(categoryList(doc, complaintCategories(), (c) => go({ kind: "complaint-form", categoryId: c.id, conversationId: v.conversationId, from: v.from })));
|
|
1281
|
+
main.appendChild(scroll);
|
|
1282
|
+
}
|
|
1283
|
+
function renderComplaintForm(v) {
|
|
1284
|
+
const category = complaintCategories().find((c) => c.id === v.categoryId);
|
|
1285
|
+
if (!category) return go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from });
|
|
1286
|
+
const back = () => complaintCategories().length > 1 && !v.conversationId ? go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from }) : go(v.from);
|
|
1287
|
+
renderHead("File a complaint", back);
|
|
1288
|
+
const scroll = el(doc, "div", "g-m-scroll");
|
|
1289
|
+
const draft2 = complaintDraft;
|
|
1290
|
+
scroll.appendChild(
|
|
1291
|
+
complaintForm(doc, {
|
|
1292
|
+
category,
|
|
1293
|
+
draft: draft2,
|
|
1294
|
+
needsEmail: needsEmail(),
|
|
1295
|
+
canChangeCategory: complaintCategories().length > 1,
|
|
1296
|
+
onChangeCategory: () => go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from }),
|
|
1297
|
+
onPickFile: (file) => {
|
|
1298
|
+
draft2.error = null;
|
|
1299
|
+
readScreenshot(doc, file).then((a) => draft2.attachment = a).catch((e) => draft2.error = e instanceof Error ? e.message : "That image couldn't be attached.").finally(() => {
|
|
1300
|
+
if (view === v) render();
|
|
1301
|
+
});
|
|
1302
|
+
},
|
|
1303
|
+
onRemoveFile: () => {
|
|
1304
|
+
draft2.attachment = null;
|
|
1305
|
+
render();
|
|
1306
|
+
},
|
|
1307
|
+
onSubmit: () => submitComplaint(v, category)
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
main.appendChild(scroll);
|
|
1311
|
+
}
|
|
1312
|
+
function submitComplaint(v, category) {
|
|
1313
|
+
const draft2 = complaintDraft;
|
|
1314
|
+
const problem = complaintProblem(category, draft2, needsEmail());
|
|
1315
|
+
draft2.checked = true;
|
|
1316
|
+
draft2.error = problem;
|
|
1317
|
+
if (problem || !api.fileComplaint) return render();
|
|
1318
|
+
draft2.sending = true;
|
|
1319
|
+
render();
|
|
1320
|
+
const answers = complaintAnswers(category, draft2);
|
|
1321
|
+
api.fileComplaint({
|
|
1322
|
+
categoryId: category.id,
|
|
1323
|
+
description: draft2.description.trim(),
|
|
1324
|
+
answers,
|
|
1325
|
+
email: needsEmail() ? draft2.email.trim() : null,
|
|
1326
|
+
conversationId: v.conversationId,
|
|
1327
|
+
attachment: draft2.attachment ? { fileName: draft2.attachment.fileName, data: draft2.attachment.data } : null
|
|
1328
|
+
}).then((res) => {
|
|
1329
|
+
upsertConversation({ ...res.conversation, unread: 0 });
|
|
1330
|
+
complaintDraft = emptyComplaintDraft();
|
|
1331
|
+
const labels = category.fields.filter((f) => answers[f.id]).map((f) => [f.label, answers[f.id]]);
|
|
1332
|
+
if (view === v) go({ kind: "complaint-sent", complaint: res.complaint, conversationId: res.conversation.id, answers: labels });
|
|
1333
|
+
}).catch((e) => {
|
|
1334
|
+
draft2.sending = false;
|
|
1335
|
+
draft2.error = (e instanceof Error && e.message ? e.message : "Your complaint couldn't be sent.").replace(/^[\w.]+: /, "");
|
|
1336
|
+
if (view === v) render();
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function renderComplaintSent(v) {
|
|
1340
|
+
renderHead(v.complaint.reference, () => go({ kind: "tab", tab: "home" }));
|
|
1341
|
+
const scroll = el(doc, "div", "g-m-scroll");
|
|
1342
|
+
scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
|
|
1343
|
+
main.appendChild(scroll);
|
|
1344
|
+
}
|
|
864
1345
|
function renderHowToView(id, from) {
|
|
865
1346
|
const cached = howToCache.get(id);
|
|
866
1347
|
renderHead(cached?.title ?? "How-to", () => go(from));
|
|
@@ -973,8 +1454,20 @@ function renderMessenger(options) {
|
|
|
973
1454
|
if (m.status === "pending") bubble.setAttribute("aria-busy", "true");
|
|
974
1455
|
bubble.dataset.messageId = m.id;
|
|
975
1456
|
wrap.appendChild(bubble);
|
|
976
|
-
|
|
1457
|
+
const offer = m.complaintOffer && canComplain() ? complaintCategories().find((c) => c.id === m.complaintOffer.categoryId) : void 0;
|
|
1458
|
+
if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer) {
|
|
977
1459
|
const chips = el(doc, "div", "g-m-chips");
|
|
1460
|
+
if (offer) {
|
|
1461
|
+
const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
|
|
1462
|
+
b.type = "button";
|
|
1463
|
+
b.append(icon(doc, "flag", "g-m-ico g-m-chip-ico"), el(doc, "span", "", `File a complaint \xB7 ${offer.name}`));
|
|
1464
|
+
b.addEventListener("click", () => {
|
|
1465
|
+
const conversationId = current?.id ?? null;
|
|
1466
|
+
complaintDraft = emptyComplaintDraft(userMessagesText());
|
|
1467
|
+
go({ kind: "complaint-form", categoryId: offer.id, conversationId, from: view });
|
|
1468
|
+
});
|
|
1469
|
+
chips.appendChild(b);
|
|
1470
|
+
}
|
|
978
1471
|
for (const g of m.suggestedGuides) {
|
|
979
1472
|
const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
|
|
980
1473
|
b.type = "button";
|
|
@@ -1004,6 +1497,7 @@ function renderMessenger(options) {
|
|
|
1004
1497
|
if (!log) return;
|
|
1005
1498
|
const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
|
|
1006
1499
|
log.textContent = "";
|
|
1500
|
+
if (current?.complaint) log.appendChild(complaintBanner(current.complaint));
|
|
1007
1501
|
if (!messages.length) {
|
|
1008
1502
|
const v = visitor();
|
|
1009
1503
|
const label = v.path ? pageLabel(v.path) : null;
|
|
@@ -1123,6 +1617,9 @@ function renderMessenger(options) {
|
|
|
1123
1617
|
sendButton = null;
|
|
1124
1618
|
if (view.kind === "thread") renderThread(view.conversationId);
|
|
1125
1619
|
else if (view.kind === "howto") renderHowToView(view.id, view.from);
|
|
1620
|
+
else if (view.kind === "complaint-category") renderComplaintCategories(view);
|
|
1621
|
+
else if (view.kind === "complaint-form") renderComplaintForm(view);
|
|
1622
|
+
else if (view.kind === "complaint-sent") renderComplaintSent(view);
|
|
1126
1623
|
else if (view.tab === "home") renderHome();
|
|
1127
1624
|
else if (view.tab === "messages") renderMessagesTab();
|
|
1128
1625
|
else if (view.tab === "news" && news().length) renderNewsTab();
|
|
@@ -1131,6 +1628,7 @@ function renderMessenger(options) {
|
|
|
1131
1628
|
renderHome();
|
|
1132
1629
|
} else renderHelpTab();
|
|
1133
1630
|
renderTabs();
|
|
1631
|
+
syncSize();
|
|
1134
1632
|
}
|
|
1135
1633
|
const handlers = (seq) => ({
|
|
1136
1634
|
onConversation(c) {
|
|
@@ -1159,8 +1657,8 @@ function renderMessenger(options) {
|
|
|
1159
1657
|
upsertMessage(m);
|
|
1160
1658
|
options.onMessage?.(m, c);
|
|
1161
1659
|
}
|
|
1162
|
-
|
|
1163
|
-
current
|
|
1660
|
+
current = withComplaint({ ...c, unread: 0 });
|
|
1661
|
+
upsertConversation(current);
|
|
1164
1662
|
},
|
|
1165
1663
|
onError() {
|
|
1166
1664
|
}
|
|
@@ -1213,8 +1711,8 @@ function renderMessenger(options) {
|
|
|
1213
1711
|
return !prev || prev.status !== m.status || prev.body !== m.body;
|
|
1214
1712
|
});
|
|
1215
1713
|
res.messages.forEach(upsertMessage);
|
|
1216
|
-
|
|
1217
|
-
current
|
|
1714
|
+
current = withComplaint({ ...res.conversation, unread: 0 });
|
|
1715
|
+
upsertConversation(current);
|
|
1218
1716
|
if (changed || messages.length !== before) {
|
|
1219
1717
|
renderLog();
|
|
1220
1718
|
res.messages.filter((m) => m.author !== "user").forEach((m) => options.onMessage?.(m, res.conversation));
|
|
@@ -1240,7 +1738,7 @@ function renderMessenger(options) {
|
|
|
1240
1738
|
api.loadMessages(next.conversationId).then((res) => {
|
|
1241
1739
|
if (seq !== threadSeq) return;
|
|
1242
1740
|
res.messages.forEach(upsertMessage);
|
|
1243
|
-
current = { ...res.conversation, unread: 0 };
|
|
1741
|
+
current = withComplaint({ ...res.conversation, unread: 0 });
|
|
1244
1742
|
upsertConversation(current);
|
|
1245
1743
|
renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
|
|
1246
1744
|
renderLog();
|
|
@@ -1278,6 +1776,7 @@ function renderMessenger(options) {
|
|
|
1278
1776
|
} else {
|
|
1279
1777
|
if (threadTimer) clearTimeout(threadTimer);
|
|
1280
1778
|
threadTimer = null;
|
|
1779
|
+
syncSize();
|
|
1281
1780
|
options.onClose?.();
|
|
1282
1781
|
}
|
|
1283
1782
|
};
|
|
@@ -1315,6 +1814,11 @@ function renderMessenger(options) {
|
|
|
1315
1814
|
setOpen(true);
|
|
1316
1815
|
openArticle(id);
|
|
1317
1816
|
},
|
|
1817
|
+
openComplaint(categoryId) {
|
|
1818
|
+
if (!canComplain()) return;
|
|
1819
|
+
setOpen(true);
|
|
1820
|
+
startComplaint(categoryId ?? null);
|
|
1821
|
+
},
|
|
1318
1822
|
refresh() {
|
|
1319
1823
|
if (destroyed) return;
|
|
1320
1824
|
if (!open) {
|