@paramms/chat-widget 1.0.44 → 1.0.45
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/README.md +4 -4
- package/dist/chatlist.d.ts +2 -1
- package/dist/chatlist.js +137 -115
- package/dist/chatlist.js.map +1 -1
- package/dist/chatlist.styles.d.ts +1 -0
- package/dist/codec.js +191 -157
- package/dist/codec.js.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/e2e.d.ts +21 -2
- package/dist/e2e.js +50 -40
- package/dist/e2e.js.map +1 -1
- package/dist/embed-lite.js +242 -0
- package/dist/embed-lite.js.map +1 -0
- package/dist/embed.d.ts +8 -0
- package/dist/embed.js +87 -78
- package/dist/embed.js.map +1 -1
- package/dist/history.d.ts +7 -4
- package/dist/index.d.ts +20 -0
- package/dist/index.js +632 -723
- package/dist/index.js.map +1 -1
- package/dist/outbox.js +65 -75
- package/dist/outbox.js.map +1 -1
- package/dist/protocol/entities.d.ts +0 -11
- package/dist/protocol/frames.d.ts +1 -17
- package/dist/react.d.ts +13 -5
- package/dist/react.js +225 -211
- package/dist/react.js.map +1 -1
- package/dist/renderer.d.ts +14 -19
- package/dist/renderer.styles.d.ts +1 -0
- package/dist/store.d.ts +1 -5
- package/dist/theme-tokens.d.ts +4 -0
- package/package.json +2 -2
- package/dist/annotations.d.ts +0 -11
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Both configure the exact same widget with the exact same field names as the Reac
|
|
|
132
132
|
data-relay-user="user_123"
|
|
133
133
|
data-relay-user-name="Jane Doe"
|
|
134
134
|
data-relay-user-email="jane@example.com"
|
|
135
|
-
data-relay-accent="#
|
|
135
|
+
data-relay-accent="#6c5ce7"
|
|
136
136
|
data-relay-position="bottom-right"
|
|
137
137
|
></script>
|
|
138
138
|
```
|
|
@@ -214,7 +214,7 @@ Same field names, all in one place, same object whether it's set on page load or
|
|
|
214
214
|
// or, for a marketplace-style card: listingTitle / listingMeta / listingPrice / listingStatus
|
|
215
215
|
// or build the card yourself: subject: { title, subtitle, tags, status }
|
|
216
216
|
|
|
217
|
-
accent: '#
|
|
217
|
+
accent: '#6c5ce7', // optional — brand colour
|
|
218
218
|
launcher: true, // optional — floating bubble vs inline; default true
|
|
219
219
|
position: 'bottom-right', // optional — 'bottom-right' | 'bottom-left'
|
|
220
220
|
launcherMessage: { title: 'Questions? Chat with us', subtitle: 'Start a conversation' }, // optional — a bare string also works (title only)
|
|
@@ -338,7 +338,7 @@ const signedToken = `${hdr}.${pay}.${sig}`
|
|
|
338
338
|
profileId="YOUR_PROFILE_ID"
|
|
339
339
|
launcher={true}
|
|
340
340
|
position="bottom-right"
|
|
341
|
-
accent="#
|
|
341
|
+
accent="#6c5ce7"
|
|
342
342
|
/>
|
|
343
343
|
```
|
|
344
344
|
|
|
@@ -401,7 +401,7 @@ RTL is detected automatically for Arabic, Hebrew, Persian and Urdu browsers.
|
|
|
401
401
|
| `user` | `UserInfo` | — | Name, email, avatar, custom metadata — shown to agents |
|
|
402
402
|
| `subjectId` | `string` | — | Item ID for marketplace mode |
|
|
403
403
|
| `showChatList` | `boolean` | `false` | Show conversation switcher in header |
|
|
404
|
-
| `accent` | `string` | `#
|
|
404
|
+
| `accent` | `string` | `#6c5ce7` | Brand colour (hex) |
|
|
405
405
|
| `launcher` | `boolean` | `false` | Render as floating button |
|
|
406
406
|
| `position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Launcher position |
|
|
407
407
|
| `launcherMessage` | `string \| { title, subtitle? }` | chatroom setting | Teaser card above the closed launcher. Overrides the dashboard's per-chatroom message; dismissible per session |
|
package/dist/chatlist.d.ts
CHANGED
|
@@ -82,8 +82,9 @@ export interface ChatListOptions {
|
|
|
82
82
|
* top-right corner — reserves header space so it doesn't sit on top of the
|
|
83
83
|
* ✎ compose button. */
|
|
84
84
|
reserveCloseSpace?: boolean;
|
|
85
|
-
/** Brand colour hex — default '#
|
|
85
|
+
/** Brand colour hex — default '#6c5ce7' */
|
|
86
86
|
accent?: string;
|
|
87
|
+
theme?: 'auto' | 'light' | 'dark';
|
|
87
88
|
/** i18n overrides */
|
|
88
89
|
i18n?: {
|
|
89
90
|
title?: string;
|
package/dist/chatlist.js
CHANGED
|
@@ -1,197 +1,219 @@
|
|
|
1
|
-
import { p as
|
|
2
|
-
function Y(o) {
|
|
3
|
-
const a = Math.floor((Date.now() - o) / 1e3);
|
|
4
|
-
if (a < 60) return "just now";
|
|
5
|
-
if (a < 3600) return `${Math.floor(a / 60)}m`;
|
|
6
|
-
if (a < 86400) return `${Math.floor(a / 3600)}h`;
|
|
7
|
-
const p = Math.floor(a / 86400);
|
|
8
|
-
if (p <= 7) return `${p}d`;
|
|
9
|
-
try {
|
|
10
|
-
return new Date(o).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
11
|
-
} catch {
|
|
12
|
-
return `${p}d`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
const Z = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
16
|
-
function t(o, a, p) {
|
|
17
|
-
const x = document.createElement(o);
|
|
18
|
-
return a && (x.className = a), p !== void 0 && (x.textContent = p), x;
|
|
19
|
-
}
|
|
1
|
+
import { l as Q, b as H, p as X, r as Y, e as F, d as Z } from "./codec.js";
|
|
20
2
|
const ee = `
|
|
21
|
-
.ocl {
|
|
3
|
+
.ocl { ${Q("ocl")}
|
|
22
4
|
display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);
|
|
23
|
-
font-family
|
|
24
|
-
|
|
5
|
+
font-family:var(--ocl-fb); color:var(--ocl-ink); overflow:hidden; }
|
|
6
|
+
@media (prefers-color-scheme: dark) { .ocl:not([data-theme="light"]) { ${H("ocl")} } }
|
|
7
|
+
.ocl[data-theme="dark"] { ${H("ocl")} }
|
|
8
|
+
.ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }
|
|
25
9
|
.ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }
|
|
26
10
|
.ocl-title svg { width:24px; height:24px; display:block; }
|
|
27
|
-
.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color
|
|
28
|
-
.ocl-search-wrap { padding:
|
|
29
|
-
.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-
|
|
11
|
+
.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 20px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
|
|
12
|
+
.ocl-search-wrap { padding:4px 12px 8px; background:var(--ocl-bg); }
|
|
13
|
+
.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-tint); border-radius:20px; padding:9px 14px; font:inherit; font-size:14px; outline:none; }
|
|
30
14
|
.ocl-body { flex:1; overflow-y:auto; }
|
|
31
|
-
.ocl-section { padding:
|
|
15
|
+
.ocl-section { padding:10px 18px 4px; font-family:var(--ocl-fh); font-size:11px; font-weight:600; color:var(--ocl-mut); text-transform:uppercase; letter-spacing:.5px; background:transparent; }
|
|
32
16
|
.ocl-empty { padding:40px 20px; text-align:center; color:var(--ocl-mut); font-size:14px; }
|
|
33
|
-
.ocl-row { display:flex; align-items:center; gap:12px; padding:
|
|
34
|
-
.ocl-row:hover { background
|
|
35
|
-
.ocl-row.unread { background:var(--ocl-
|
|
36
|
-
.ocl-av { width:
|
|
37
|
-
.ocl-dot { position:absolute; bottom:1px; right:1px; width:12px; height:12px; border-radius:50%; border:2px solid var(--ocl-card); background:var(--ocl-mut); }
|
|
38
|
-
.ocl-dot.open { background:#22c55e; }
|
|
39
|
-
.ocl-dot.waiting { background:#f59e0b; }
|
|
17
|
+
.ocl-row { display:flex; align-items:center; gap:12px; padding:10px 12px; margin:6px 12px; background:var(--ocl-tint); border:none; width:calc(100% - 24px); box-sizing:border-box; border-radius:16px; text-align:left; cursor:pointer; transition:background .12s; }
|
|
18
|
+
.ocl-row:hover { background:var(--ocl-rowhover); }
|
|
19
|
+
.ocl-row.unread { background:var(--ocl-rowhover); }
|
|
20
|
+
.ocl-av { width:40px; height:40px; border-radius:50%; background:var(--ocl-accent); color:var(--ocl-onaccent); font-family:var(--ocl-fh); font-size:15px; font-weight:600; display:flex; align-items:center; justify-content:center; flex:none; }
|
|
40
21
|
.ocl-info { flex:1; min-width:0; }
|
|
41
|
-
.ocl-name { font-size:
|
|
22
|
+
.ocl-name { font-family:var(--ocl-fh); font-size:14px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }
|
|
42
23
|
.ocl-row.unread .ocl-name { font-weight:700; }
|
|
43
|
-
.ocl-preview { font-size:
|
|
24
|
+
.ocl-preview { font-size:11.5px; color:var(--ocl-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
44
25
|
.ocl-row.unread .ocl-preview { color:var(--ocl-ink); }
|
|
45
26
|
.ocl-right { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex:none; }
|
|
46
|
-
.ocl-time { font-size:
|
|
27
|
+
.ocl-time { font-size:10px; color:var(--ocl-mut); }
|
|
47
28
|
.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }
|
|
48
|
-
.ocl-badge { background:var(--ocl-accent); color
|
|
29
|
+
.ocl-badge { background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:999px; font-size:11px; font-weight:700; min-width:20px; height:20px; padding:0 5px; display:flex; align-items:center; justify-content:center; }
|
|
30
|
+
.ocl-status { font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }
|
|
31
|
+
.ocl-status.open { background:#dff3e6; color:#2f8a52; }
|
|
32
|
+
.ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }
|
|
33
|
+
.ocl-status.done { background:#eee; color:#777; }
|
|
34
|
+
.ocl[data-theme="dark"] .ocl-status.done { background:#39325e; color:#b3abd6; }
|
|
35
|
+
@media (prefers-color-scheme: dark) { .ocl:not([data-theme="light"]) .ocl-status.done { background:#39325e; color:#b3abd6; } }
|
|
49
36
|
.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }
|
|
50
|
-
.ocl-compose { border:none; background:var(--ocl-
|
|
51
|
-
.ocl-compose:hover {
|
|
37
|
+
.ocl-compose { border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); width:32px; height:32px; border-radius:50%; font-size:18px; line-height:1; cursor:pointer; box-shadow:var(--ocl-shadow); }
|
|
38
|
+
.ocl-compose:hover { filter:brightness(1.08); }
|
|
52
39
|
/* When the HOST overlays a ✕ (ChatApp's onClose / embed's inbox stack), it is
|
|
53
40
|
absolutely positioned at top-right and used to land straight on top of the ✎
|
|
54
41
|
compose button. Reserve the space instead of stacking them. */
|
|
55
42
|
.ocl-has-close .ocl-head { padding-right:52px; }
|
|
56
|
-
.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color
|
|
43
|
+
.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
|
|
57
44
|
/* Compact sizing keyed on the LIST'S OWN width (ResizeObserver toggles
|
|
58
45
|
* .ocl-compact below 400px) — covers phones and narrow desktop embeds alike.
|
|
59
46
|
* Search must be ≥16px in compact or iOS zooms the page on focus. */
|
|
60
47
|
.ocl-compact .ocl-row { padding:13px 14px; }
|
|
61
48
|
.ocl-compact .ocl-search { font-size:16px; }
|
|
62
49
|
.ocl-compact .ocl-compose { width:36px; height:36px; }
|
|
50
|
+
.ocl-row:focus-visible, .ocl button:focus-visible, .ocl input:focus-visible { outline:2px solid var(--ocl-accent); outline-offset:2px; border-radius:12px; }
|
|
63
51
|
`;
|
|
52
|
+
function oe(o) {
|
|
53
|
+
switch (o) {
|
|
54
|
+
case "open":
|
|
55
|
+
return { label: "Open", cls: "open" };
|
|
56
|
+
case "awaiting_staff":
|
|
57
|
+
return { label: "Waiting on you", cls: "waiting" };
|
|
58
|
+
case "resolved":
|
|
59
|
+
return { label: "Resolved", cls: "done" };
|
|
60
|
+
case "closed":
|
|
61
|
+
return { label: "Closed", cls: "done" };
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
64
66
|
function te(o) {
|
|
65
|
-
const
|
|
67
|
+
const c = Math.floor((Date.now() - o) / 1e3);
|
|
68
|
+
if (c < 60) return "just now";
|
|
69
|
+
if (c < 3600) return `${Math.floor(c / 60)}m`;
|
|
70
|
+
if (c < 86400) return `${Math.floor(c / 3600)}h`;
|
|
71
|
+
const p = Math.floor(c / 86400);
|
|
72
|
+
if (p <= 7) return `${p}d`;
|
|
73
|
+
try {
|
|
74
|
+
return new Date(o).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
75
|
+
} catch {
|
|
76
|
+
return `${p}d`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const ne = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
80
|
+
function t(o, c, p) {
|
|
81
|
+
const x = document.createElement(o);
|
|
82
|
+
return c && (x.className = c), p !== void 0 && (x.textContent = p), x;
|
|
83
|
+
}
|
|
84
|
+
function ae(o) {
|
|
85
|
+
const c = o.token ?? o.userId ?? X(), { httpBase: p, wsUrl: x } = Y(o.url, o.apiUrl), f = o.i18n ?? {}, G = o.accent ?? "#6c5ce7";
|
|
66
86
|
if (!document.getElementById("ocl-styles")) {
|
|
67
87
|
const e = document.createElement("style");
|
|
68
88
|
e.id = "ocl-styles", e.textContent = ee, document.head.append(e);
|
|
69
89
|
}
|
|
70
90
|
const h = t("div", "ocl");
|
|
71
|
-
h.style.setProperty("--ocl-accent",
|
|
72
|
-
const
|
|
73
|
-
if (C.setAttribute("role", "img"), C.setAttribute("aria-label", f.title ?? "Messages"), C.innerHTML =
|
|
91
|
+
h.style.setProperty("--ocl-accent", G), o.theme && o.theme !== "auto" && (h.dataset.theme = o.theme), o.reserveCloseSpace && h.classList.add("ocl-has-close");
|
|
92
|
+
const z = t("div", "ocl-head"), C = t("span", "ocl-title");
|
|
93
|
+
if (C.setAttribute("role", "img"), C.setAttribute("aria-label", f.title ?? "Messages"), C.innerHTML = ne, z.append(C), o.onNewChat) {
|
|
74
94
|
const e = t("button", "ocl-compose", "✎");
|
|
75
|
-
e.title = f.newChat ?? "New conversation", e.addEventListener("click", () => o.onNewChat()),
|
|
95
|
+
e.title = f.newChat ?? "New conversation", e.addEventListener("click", () => o.onNewChat()), z.append(e);
|
|
76
96
|
}
|
|
77
|
-
const R = t("div", "ocl-search-wrap"),
|
|
78
|
-
|
|
97
|
+
const R = t("div", "ocl-search-wrap"), v = t("input", "ocl-search");
|
|
98
|
+
v.placeholder = f.search ?? "🔍 Search", v.type = "search", R.append(v);
|
|
79
99
|
const s = t("div", "ocl-body");
|
|
80
|
-
s.append(t("div", "ocl-spinner", "Loading…")), h.append(
|
|
81
|
-
const
|
|
100
|
+
s.append(t("div", "ocl-spinner", "Loading…")), h.append(z, R, s), o.el.replaceChildren(h);
|
|
101
|
+
const T = (e) => {
|
|
82
102
|
h.classList.toggle("ocl-compact", e > 0 && e < 400);
|
|
83
103
|
};
|
|
84
|
-
|
|
85
|
-
let
|
|
86
|
-
if (typeof ResizeObserver < "u" && (
|
|
104
|
+
T(h.clientWidth);
|
|
105
|
+
let m = null;
|
|
106
|
+
if (typeof ResizeObserver < "u" && (m = new ResizeObserver((e) => {
|
|
87
107
|
var r;
|
|
88
|
-
return
|
|
89
|
-
}),
|
|
90
|
-
let
|
|
91
|
-
const
|
|
108
|
+
return T(((r = e[0]) == null ? void 0 : r.contentRect.width) ?? h.clientWidth);
|
|
109
|
+
}), m.observe(h)), !o.profileId && !o.tenantId) throw new Error("[relay chatlist] provide profileId or tenantId");
|
|
110
|
+
let M;
|
|
111
|
+
const N = `ocl_seen_${o.profileId ?? `t_${o.tenantId}`}_${(o.userId ?? c).slice(-8)}`;
|
|
92
112
|
let b = {};
|
|
93
113
|
try {
|
|
94
|
-
b = JSON.parse(localStorage.getItem(
|
|
114
|
+
b = JSON.parse(localStorage.getItem(N) ?? "{}");
|
|
95
115
|
} catch {
|
|
96
116
|
}
|
|
97
|
-
const
|
|
117
|
+
const J = () => {
|
|
98
118
|
try {
|
|
99
|
-
localStorage.setItem(
|
|
119
|
+
localStorage.setItem(N, JSON.stringify(b));
|
|
100
120
|
} catch {
|
|
101
121
|
}
|
|
102
122
|
};
|
|
103
|
-
let
|
|
104
|
-
const
|
|
105
|
-
const e = o.profileId ? `profileId=${encodeURIComponent(o.profileId)}${o.scope === "tenant" ? "&scope=tenant" : ""}` : `tenantId=${encodeURIComponent(o.tenantId)}`, r = `${p}/conversations/mine?${e}`,
|
|
123
|
+
let E = [], g = !1;
|
|
124
|
+
const K = async () => {
|
|
125
|
+
const e = o.profileId ? `profileId=${encodeURIComponent(o.profileId)}${o.scope === "tenant" ? "&scope=tenant" : ""}` : `tenantId=${encodeURIComponent(o.tenantId)}`, r = `${p}/conversations/mine?${e}`, l = { authorization: `Bearer ${c}` };
|
|
106
126
|
let d;
|
|
107
127
|
try {
|
|
108
|
-
d = await fetch(r, { headers:
|
|
128
|
+
d = await fetch(r, { headers: l });
|
|
109
129
|
} catch {
|
|
110
|
-
d = await fetch(r, { headers:
|
|
130
|
+
d = await fetch(r, { headers: l });
|
|
111
131
|
}
|
|
112
132
|
if (!d.ok) return [];
|
|
113
|
-
const
|
|
114
|
-
return
|
|
115
|
-
},
|
|
133
|
+
const i = await d.json();
|
|
134
|
+
return i.defaultProfileId && (M = i.defaultProfileId), (i.conversations ?? []).sort((u, n) => n.updatedAt - u.updatedAt);
|
|
135
|
+
}, O = (e, r) => {
|
|
116
136
|
if (g) return;
|
|
117
|
-
const
|
|
118
|
-
(n) =>
|
|
137
|
+
const l = r ? e.filter(
|
|
138
|
+
(n) => U(n).toLowerCase().includes(r) || (n.lastMessage ?? "").toLowerCase().includes(r)
|
|
119
139
|
) : e;
|
|
120
|
-
if (s.replaceChildren(), !
|
|
140
|
+
if (s.replaceChildren(), !l.length) {
|
|
121
141
|
const n = t("div", "ocl-empty", r ? "No results." : f.empty ?? "No conversations yet.");
|
|
122
142
|
if (!r && o.onNewChat) {
|
|
123
143
|
n.append(t("br"));
|
|
124
|
-
const
|
|
125
|
-
|
|
144
|
+
const S = t("button", "ocl-start", f.newChat ?? "Start a conversation");
|
|
145
|
+
S.addEventListener("click", () => o.onNewChat()), n.append(S);
|
|
126
146
|
}
|
|
127
147
|
s.append(n);
|
|
128
148
|
return;
|
|
129
149
|
}
|
|
130
|
-
const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0),
|
|
131
|
-
if (
|
|
132
|
-
s.append(t("div", "ocl-section", `${f.unread ?? "Unread"} (${
|
|
133
|
-
for (const n of
|
|
150
|
+
const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0), i = l.filter(d), u = l.filter((n) => !d(n));
|
|
151
|
+
if (i.length) {
|
|
152
|
+
s.append(t("div", "ocl-section", `${f.unread ?? "Unread"} (${i.length})`));
|
|
153
|
+
for (const n of i) s.append(A(n, d(n)));
|
|
134
154
|
}
|
|
135
155
|
if (u.length) {
|
|
136
|
-
s.append(t("div", "ocl-section",
|
|
137
|
-
for (const n of u) s.append(
|
|
156
|
+
s.append(t("div", "ocl-section", i.length ? f.all ?? "All conversations" : ""));
|
|
157
|
+
for (const n of u) s.append(A(n, !1));
|
|
138
158
|
}
|
|
139
|
-
},
|
|
140
|
-
var
|
|
141
|
-
const
|
|
142
|
-
|
|
159
|
+
}, U = (e) => e.subjectTitle ?? (e.kind === "direct" ? e.peerId ?? "Direct message" : "General enquiry"), A = (e, r) => {
|
|
160
|
+
var P;
|
|
161
|
+
const l = U(e), d = (((P = l.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : P[0]) ?? l.trim()[0] ?? "?").toUpperCase(), i = e.lastSeq ?? 0, u = r ? Math.max(1, i - (b[e.id] ?? 0)) : 0, n = t("button", `ocl-row${r ? " unread" : ""}`), S = t("div", "ocl-av", d);
|
|
162
|
+
n.append(S);
|
|
143
163
|
const $ = t("div", "ocl-info");
|
|
144
|
-
$.append(t("div", "ocl-name",
|
|
145
|
-
const
|
|
164
|
+
$.append(t("div", "ocl-name", l));
|
|
165
|
+
const V = {
|
|
146
166
|
open: "Open",
|
|
147
167
|
awaiting_staff: "Waiting for reply…",
|
|
148
168
|
resolved: "Resolved ✓",
|
|
149
169
|
closed: "Closed"
|
|
150
170
|
};
|
|
151
|
-
$.append(t("div", "ocl-preview", e.lastMessage ??
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
$.append(t("div", "ocl-preview", e.lastMessage ?? V[e.state] ?? e.state)), n.append($);
|
|
172
|
+
const k = t("div", "ocl-right");
|
|
173
|
+
k.append(t("div", "ocl-time", te(e.updatedAt)));
|
|
174
|
+
const L = oe(e.state);
|
|
175
|
+
return L && k.append(t("div", `ocl-status ${L.cls}`, L.label)), u > 0 && k.append(t("div", "ocl-badge", String(u > 99 ? "99+" : u))), n.append(k), n.addEventListener("click", () => {
|
|
176
|
+
var q;
|
|
177
|
+
i > 0 && (b[e.id] = i, J()), n.classList.remove("unread"), (q = k.querySelector(".ocl-badge")) == null || q.remove(), o.onSelect(e);
|
|
156
178
|
}), n;
|
|
157
179
|
}, I = () => {
|
|
158
|
-
g ||
|
|
159
|
-
g || (
|
|
180
|
+
g || K().then((e) => {
|
|
181
|
+
g || (E = e, O(e, v.value.trim().toLowerCase()));
|
|
160
182
|
}).catch((e) => {
|
|
161
183
|
if (g) return;
|
|
162
184
|
console.error(`[chat-widget] failed to load conversations from ${p}/conversations/mine — check the apiUrl/CORS config.`, e);
|
|
163
185
|
const r = t("div", "ocl-empty", f.error ?? "Could not load conversations.");
|
|
164
186
|
r.append(t("br"));
|
|
165
|
-
const
|
|
166
|
-
|
|
187
|
+
const l = t("button", "ocl-retry", f.retry ?? "Retry");
|
|
188
|
+
l.addEventListener("click", () => {
|
|
167
189
|
s.replaceChildren(t("div", "ocl-spinner", "Loading…")), I();
|
|
168
|
-
}), r.append(
|
|
190
|
+
}), r.append(l), s.replaceChildren(r);
|
|
169
191
|
});
|
|
170
192
|
};
|
|
171
|
-
|
|
172
|
-
let
|
|
173
|
-
const
|
|
193
|
+
v.addEventListener("input", () => O(E, v.value.trim().toLowerCase())), I();
|
|
194
|
+
let a = null, w, y, _ = 0, W = !1;
|
|
195
|
+
const j = () => {
|
|
174
196
|
y || (y = setTimeout(() => {
|
|
175
197
|
y = void 0, I();
|
|
176
198
|
}, 300));
|
|
177
|
-
},
|
|
199
|
+
}, B = () => {
|
|
178
200
|
if (!g) {
|
|
179
201
|
try {
|
|
180
|
-
|
|
202
|
+
a = new WebSocket(x);
|
|
181
203
|
} catch {
|
|
182
204
|
D();
|
|
183
205
|
return;
|
|
184
206
|
}
|
|
185
|
-
|
|
186
|
-
_ = 0,
|
|
187
|
-
},
|
|
188
|
-
const r =
|
|
189
|
-
r && r.type === "inbox_event" &&
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
},
|
|
207
|
+
a.binaryType = "arraybuffer", a.onopen = () => {
|
|
208
|
+
_ = 0, a.send(F({ type: "auth", token: c })), a.send(F({ type: "subscribe_inbox" })), W && j(), W = !0;
|
|
209
|
+
}, a.onmessage = (e) => {
|
|
210
|
+
const r = Z(new Uint8Array(e.data));
|
|
211
|
+
r && r.type === "inbox_event" && j();
|
|
212
|
+
}, a.onclose = () => {
|
|
213
|
+
a = null, D();
|
|
214
|
+
}, a.onerror = () => {
|
|
193
215
|
try {
|
|
194
|
-
|
|
216
|
+
a == null || a.close();
|
|
195
217
|
} catch {
|
|
196
218
|
}
|
|
197
219
|
};
|
|
@@ -200,25 +222,25 @@ function te(o) {
|
|
|
200
222
|
if (g || w) return;
|
|
201
223
|
const e = Math.min(15e3, 500 * 2 ** _++) + Math.random() * 250;
|
|
202
224
|
w = setTimeout(() => {
|
|
203
|
-
w = void 0,
|
|
225
|
+
w = void 0, B();
|
|
204
226
|
}, e);
|
|
205
227
|
};
|
|
206
|
-
return
|
|
228
|
+
return B(), {
|
|
207
229
|
refresh: I,
|
|
208
230
|
close() {
|
|
209
|
-
g = !0, w && clearTimeout(w), y && clearTimeout(y),
|
|
231
|
+
g = !0, w && clearTimeout(w), y && clearTimeout(y), m == null || m.disconnect(), m = null;
|
|
210
232
|
try {
|
|
211
|
-
|
|
233
|
+
a == null || a.close();
|
|
212
234
|
} catch {
|
|
213
235
|
}
|
|
214
|
-
|
|
236
|
+
a = null, o.el.replaceChildren();
|
|
215
237
|
},
|
|
216
238
|
defaultProfileId() {
|
|
217
|
-
return o.profileId ??
|
|
239
|
+
return o.profileId ?? M;
|
|
218
240
|
}
|
|
219
241
|
};
|
|
220
242
|
}
|
|
221
243
|
export {
|
|
222
|
-
|
|
244
|
+
ae as mountChatList
|
|
223
245
|
};
|
|
224
246
|
//# sourceMappingURL=chatlist.js.map
|
package/dist/chatlist.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatlist.js","sources":["../src/chatlist.ts"],"sourcesContent":["/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: 'https://api.relay.paramms.com', // ONE url, any scheme\n * profileId: 'p_usedcars',\n * userId: currentUser.id, // optional — uses localStorage UID if omitted\n * onSelect: (entry) => {\n * window.location.href = `/listings/${entry.subjectId}#chat`\n * },\n * })\n * handle.refresh() // manually re-fetch the list\n * handle.close() // unmount and clean up\n */\n\nimport { resolveRelayUrls } from './history.js'\nimport { persistentUid } from './uid.js'\nimport { encodeFrame, decodeFrame } from './protocol/codec.js'\n\nexport interface ChatListEntry {\n id: string\n /** Chatroom this conversation belongs to. With `scope: 'tenant'` this can\n * differ from the profileId the list was mounted with — open the chat\n * against THIS profileId. */\n profileId?: string\n /** 'support' (default) or 'direct' (user↔user DM). */\n kind?: string\n /** For direct conversations: the other participant's user id. */\n peerId?: string\n subjectId?: string\n subjectTitle?: string\n /** One-line detail — e.g. \"45,000 km · Auto\" */\n subjectMeta?: string\n /** URL of the listing/item page — stored automatically when the widget first opens */\n subjectUrl?: string\n state: string\n updatedAt: number\n lastSeq?: number\n lastMessage?: string\n}\n\nexport interface ChatListOptions {\n /** Mount target element */\n el: HTMLElement\n /** Relay URL — ONE url, any scheme (https recommended). The WebSocket URL\n * and REST base are derived automatically. */\n url: string\n /** HTTP(S) base for REST — only when REST is on a different origin.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Profile ID to scope conversations to */\n profileId?: string\n /** Tenant-level identification — list EVERY conversation this user has\n * with the business, across ALL of its chatrooms, without naming one\n * (e.g. a platform running a marketplace chatroom AND a general-support\n * chatroom). Provide `profileId` OR `tenantId` (profileId wins if both;\n * it also fixes where \"new conversation\" opens). With only `tenantId`,\n * the compose target is the server-reported defaultProfileId (the\n * tenant's oldest chatroom). */\n tenantId?: string\n /** A signed identity token (ES256 JWT) — the production identity tier for\n * chatrooms with signed identity enabled. Wins over `userId`. */\n token?: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Which conversations to list (default 'profile'):\n * 'profile' — only this chatroom's threads.\n * 'tenant' — every conversation this user has with the chatroom's owning\n * business, across ALL of its chatrooms (a real chat-app inbox). Rows\n * carry `profileId` so each opens against the right chatroom. */\n scope?: 'profile' | 'tenant'\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** When provided, the list shows a ✎ compose button in the header (and a\n * \"Start a conversation\" button in the empty state) that calls this —\n * wire it to open a fresh/general thread. Without it a user with no\n * conversations yet has nothing to tap. */\n onNewChat?: () => void\n /** Set when the CALLER draws its own close (✕) control overlaying the list's\n * top-right corner — reserves header space so it doesn't sit on top of the\n * ✎ compose button. */\n reserveCloseSpace?: boolean\n /** Brand colour hex — default '#4F63F5' */\n accent?: string\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n retry?: string // default 'Retry'\n close?: string // default 'Close' (aria-label for the ✕ control)\n newChat?: string // default 'New conversation' / 'Start a conversation'\n }\n}\n\nexport interface ChatListHandle {\n /** Re-fetch and re-render the list */\n refresh(): void\n /** Unmount and clean up */\n close(): void\n /** Where \"new conversation\" should open: the configured profileId, else the\n * server-reported tenant default (oldest chatroom). Undefined until the\n * first successful fetch when only tenantId was configured. */\n defaultProfileId(): string | undefined\n}\n\nfunction timeAgo(ts: number): string {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return 'just now'\n if (s < 3600) return `${Math.floor(s / 60)}m`\n if (s < 86400) return `${Math.floor(s / 3600)}h`\n const days = Math.floor(s / 86400)\n if (days <= 7) return `${days}d`\n // Beyond a week, a date scans better than \"43d\" (what Channel.io/Intercom do).\n try { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) }\n catch { return `${days}d` }\n}\n\n// Inline, dependency-free chat-bubble glyph. `currentColor` picks up the\n// header's accent tint. No external font/emoji so it renders identically\n// across platforms (the old '💬'/text wordmark varied per-OS).\nconst CHAT_ICON_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\" ' +\n 'fill=\"currentColor\" fill-opacity=\"0.12\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'\n\nfunction el(tag: string, cls?: string, text?: string): HTMLElement {\n const e = document.createElement(tag)\n if (cls) e.className = cls\n if (text !== undefined) e.textContent = text\n return e\n}\n\nconst CSS = `\n.ocl { --ocl-accent:#f5713c; --ocl-bg:#f3efe9; --ocl-card:#fff; --ocl-line:#ececec; --ocl-ink:#1c1b1a; --ocl-mut:#9b9690;\n display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);\n font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; color:var(--ocl-ink); overflow:hidden; }\n.ocl-head { display:flex; align-items:center; padding:14px 16px 10px; background:var(--ocl-card); border-bottom:1px solid var(--ocl-line); }\n.ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }\n.ocl-title svg { width:24px; height:24px; display:block; }\n.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color:#fff; border-radius:20px; padding:8px 20px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n.ocl-search-wrap { padding:8px 12px; background:var(--ocl-card); border-bottom:1px solid var(--ocl-line); }\n.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-bg); border-radius:20px; padding:8px 14px; font:inherit; font-size:14px; outline:none; }\n.ocl-body { flex:1; overflow-y:auto; }\n.ocl-section { padding:6px 16px 4px; font-size:11px; font-weight:700; color:var(--ocl-mut); text-transform:uppercase; letter-spacing:.5px; background:var(--ocl-bg); }\n.ocl-empty { padding:40px 20px; text-align:center; color:var(--ocl-mut); font-size:14px; }\n.ocl-row { display:flex; align-items:center; gap:12px; padding:11px 16px; background:var(--ocl-card); border:none; width:100%; text-align:left; cursor:pointer; border-bottom:1px solid var(--ocl-line); transition:background .1s; }\n.ocl-row:hover { background:#f7f5f2; }\n.ocl-row.unread { background:var(--ocl-card); }\n.ocl-av { width:46px; height:46px; border-radius:50%; background:var(--ocl-accent); color:#fff; font-size:18px; font-weight:700; display:flex; align-items:center; justify-content:center; flex:none; position:relative; }\n.ocl-dot { position:absolute; bottom:1px; right:1px; width:12px; height:12px; border-radius:50%; border:2px solid var(--ocl-card); background:var(--ocl-mut); }\n.ocl-dot.open { background:#22c55e; }\n.ocl-dot.waiting { background:#f59e0b; }\n.ocl-info { flex:1; min-width:0; }\n.ocl-name { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }\n.ocl-row.unread .ocl-name { font-weight:700; }\n.ocl-preview { font-size:13px; color:var(--ocl-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.ocl-row.unread .ocl-preview { color:var(--ocl-ink); }\n.ocl-right { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex:none; }\n.ocl-time { font-size:11px; color:var(--ocl-mut); }\n.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }\n.ocl-badge { background:var(--ocl-accent); color:#fff; border-radius:999px; font-size:11px; font-weight:700; min-width:20px; height:20px; padding:0 5px; display:flex; align-items:center; justify-content:center; }\n.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }\n.ocl-compose { border:none; background:var(--ocl-bg); color:var(--ocl-ink); width:32px; height:32px; border-radius:50%; font-size:15px; cursor:pointer; }\n.ocl-compose:hover { background:var(--ocl-line); }\n/* When the HOST overlays a ✕ (ChatApp's onClose / embed's inbox stack), it is\n absolutely positioned at top-right and used to land straight on top of the ✎\n compose button. Reserve the space instead of stacking them. */\n.ocl-has-close .ocl-head { padding-right:52px; }\n.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:#fff; border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n/* Compact sizing keyed on the LIST'S OWN width (ResizeObserver toggles\n * .ocl-compact below 400px) — covers phones and narrow desktop embeds alike.\n * Search must be ≥16px in compact or iOS zooms the page on focus. */\n.ocl-compact .ocl-row { padding:13px 14px; }\n.ocl-compact .ocl-search { font-size:16px; }\n.ocl-compact .ocl-compose { width:36px; height:36px; }\n`\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.token ?? opts.userId ?? persistentUid()\n const { httpBase, wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#4F63F5'\n\n // Inject the shared stylesheet ONCE, and keep it accent-FREE. The accent was\n // previously baked into this shared <style> (CSS.replace(/#f5713c/g, accent)),\n // which meant the FIRST list mounted on a page won the accent for EVERY list\n // after it (the style tag already existed, so a second list's colour was\n // ignored) — real interference when a project runs more than one widget. The\n // accent now lives in a per-instance CSS variable set on the root element\n // below, so each list keeps its own colour and the shared sheet stays static\n // (also friendlier to HTTP caching).\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS\n document.head.append(s)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n root.style.setProperty('--ocl-accent', accent) // per-instance accent\n if (opts.reserveCloseSpace) root.classList.add('ocl-has-close')\n const head = el('div', 'ocl-head')\n // Header shows a chat icon (accent-tinted) rather than a \"Messages\" wordmark.\n // The i18n title is still exposed as the accessible label for screen readers.\n const titleEl = el('span', 'ocl-title')\n titleEl.setAttribute('role', 'img')\n titleEl.setAttribute('aria-label', i18n.title ?? 'Messages')\n titleEl.innerHTML = CHAT_ICON_SVG\n head.append(titleEl)\n if (opts.onNewChat) {\n const compose = el('button', 'ocl-compose', '✎') as HTMLButtonElement\n compose.title = i18n.newChat ?? 'New conversation'\n compose.addEventListener('click', () => opts.onNewChat!())\n head.append(compose)\n }\n\n const searchWrap = el('div', 'ocl-search-wrap')\n const searchIn = el('input', 'ocl-search') as HTMLInputElement\n searchIn.placeholder = i18n.search ?? '🔍 Search'; searchIn.type = 'search'\n searchWrap.append(searchIn)\n\n const body = el('div', 'ocl-body')\n body.append(el('div', 'ocl-spinner', 'Loading…'))\n root.append(head, searchWrap, body)\n opts.el.replaceChildren(root)\n\n // Container-driven compact sizing (see the CSS note). Guarded for\n // jsdom/old runtimes without ResizeObserver — they keep desktop sizing.\n const applyCompact = (w: number): void => { root.classList.toggle('ocl-compact', w > 0 && w < 400) }\n applyCompact(root.clientWidth)\n let compactObserver: ResizeObserver | null = null\n if (typeof ResizeObserver !== 'undefined') {\n compactObserver = new ResizeObserver((entries) => applyCompact(entries[0]?.contentRect.width ?? root.clientWidth))\n compactObserver.observe(root)\n }\n\n // Track seen seqs for unread counts (persisted in localStorage)\n // Key unread tracking by the STABLE id (userId beats token here: a signed\n // JWT changes every mint, which would reset unread counts on each load).\n if (!opts.profileId && !opts.tenantId) throw new Error('[relay chatlist] provide profileId or tenantId')\n let serverDefaultProfileId: string | undefined\n const seenKey = `ocl_seen_${opts.profileId ?? `t_${opts.tenantId}`}_${(opts.userId ?? token).slice(-8)}`\n let seenSeq: Record<string, number> = {}\n try { seenSeq = JSON.parse(localStorage.getItem(seenKey) ?? '{}') } catch {}\n\n const saveSeenSeq = () => {\n try { localStorage.setItem(seenKey, JSON.stringify(seenSeq)) } catch {}\n }\n\n let allEntries: ChatListEntry[] = []\n let destroyed = false\n\n // Fetch conversations from server\n const fetchEntries = async (): Promise<ChatListEntry[]> => {\n const who = opts.profileId\n ? `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n : `tenantId=${encodeURIComponent(opts.tenantId!)}` // tenant-level is inherently tenant-scoped\n const url = `${httpBase}/conversations/mine?${who}`\n const headers = { authorization: `Bearer ${token}` }\n // A GET is safe to repeat. A single transient network reject — the socket\n // still warming up right after a reload, a relay that blipped — used to\n // dead-end straight to \"Could not load conversations.\" Retry once so a blip\n // self-heals; an HTTP error (4xx/5xx) is NOT a network failure and returns\n // an empty list rather than retrying or erroring.\n let res: Response\n try {\n res = await fetch(url, { headers })\n } catch {\n res = await fetch(url, { headers }) // one immediate retry\n }\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[]; defaultProfileId?: string }\n if (data.defaultProfileId) serverDefaultProfileId = data.defaultProfileId\n return (data.conversations ?? []).sort((a, b) => b.updatedAt - a.updatedAt)\n }\n\n // Render rows from entries, optionally filtered by search query\n const renderRows = (entries: ChatListEntry[], query: string) => {\n if (destroyed) return\n const filtered = query\n ? entries.filter(e =>\n rowName(e).toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n const empty = el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.'))\n if (!query && opts.onNewChat) {\n empty.append(el('br'))\n const start = el('button', 'ocl-start', i18n.newChat ?? 'Start a conversation') as HTMLButtonElement\n start.addEventListener('click', () => opts.onNewChat!())\n empty.append(start)\n }\n body.append(empty)\n return\n }\n\n const isUnread = (e: ChatListEntry) =>\n (e.lastSeq ?? 0) > (seenSeq[e.id] ?? 0)\n\n const unread = filtered.filter(isUnread)\n const read = filtered.filter(e => !isUnread(e))\n\n if (unread.length) {\n body.append(el('div', 'ocl-section', `${i18n.unread ?? 'Unread'} (${unread.length})`))\n for (const e of unread) body.append(buildRow(e, isUnread(e)))\n }\n if (read.length) {\n body.append(el('div', 'ocl-section', unread.length ? (i18n.all ?? 'All conversations') : ''))\n for (const e of read) body.append(buildRow(e, false))\n }\n }\n\n const rowName = (entry: ChatListEntry): string =>\n entry.subjectTitle ?? (entry.kind === 'direct' ? (entry.peerId ?? 'Direct message') : 'General enquiry')\n\n const buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = rowName(entry)\n // Prefer the first LETTER (any script), not a leading digit/symbol — a\n // listing titled \"2018 Kia K7\" should show \"K\", not \"2\", and Korean/other\n // scripts pick their first character too.\n const initial = (name.match(/\\p{L}/u)?.[0] ?? name.trim()[0] ?? '?').toUpperCase()\n const lastSeq = entry.lastSeq ?? 0\n const unreadCount = unread ? Math.max(1, lastSeq - (seenSeq[entry.id] ?? 0)) : 0\n\n const row = el('button', `ocl-row${unread ? ' unread' : ''}`) as HTMLButtonElement\n\n // Avatar\n const av = el('div', 'ocl-av', initial)\n const dot = el('div', 'ocl-dot')\n if (entry.state === 'open') dot.classList.add('open')\n else if (entry.state === 'awaiting_staff') dot.classList.add('waiting')\n av.append(dot)\n row.append(av)\n\n // Info\n const info = el('div', 'ocl-info')\n info.append(el('div', 'ocl-name', name))\n const stateMap: Record<string, string> = {\n open: 'Open', awaiting_staff: 'Waiting for reply…',\n resolved: 'Resolved ✓', closed: 'Closed',\n }\n info.append(el('div', 'ocl-preview', entry.lastMessage ?? stateMap[entry.state] ?? entry.state))\n row.append(info)\n\n // Right\n const right = el('div', 'ocl-right')\n right.append(el('div', 'ocl-time', timeAgo(entry.updatedAt)))\n if (unreadCount > 0) {\n right.append(el('div', 'ocl-badge', String(unreadCount > 99 ? '99+' : unreadCount)))\n }\n row.append(right)\n\n row.addEventListener('click', () => {\n // Mark as read\n if (lastSeq > 0) { seenSeq[entry.id] = lastSeq; saveSeenSeq() }\n row.classList.remove('unread')\n right.querySelector('.ocl-badge')?.remove()\n opts.onSelect(entry)\n })\n\n return row\n }\n\n const refresh = () => {\n if (destroyed) return\n fetchEntries().then(entries => {\n if (destroyed) return\n allEntries = entries\n renderRows(entries, searchIn.value.trim().toLowerCase())\n }).catch((e) => {\n if (destroyed) return\n console.error(`[chat-widget] failed to load conversations from ${httpBase}/conversations/mine — check the apiUrl/CORS config.`, e)\n const errBox = el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.')\n errBox.append(el('br'))\n const retry = el('button', 'ocl-retry', i18n.retry ?? 'Retry') as HTMLButtonElement\n retry.addEventListener('click', () => {\n body.replaceChildren(el('div', 'ocl-spinner', 'Loading…'))\n refresh()\n })\n errBox.append(retry)\n body.replaceChildren(errBox)\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n // ── Live inbox: subscribe over WS so the list updates the instant any of the\n // guest's conversations changes, instead of only on the periodic poll. The\n // server streams `inbox_event` to the guest's OWN inbox (keyed by guestId).\n // We coalesce bursts and re-fetch (the fetch already sorts/dedupes); the poll\n // stays as a backstop for a dropped socket. ────────────────────────────────\n let sock: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let refreshTimer: ReturnType<typeof setTimeout> | undefined\n let attempt = 0\n let connectedBefore = false\n const debouncedRefresh = () => {\n if (refreshTimer) return\n refreshTimer = setTimeout(() => { refreshTimer = undefined; refresh() }, 300)\n }\n const connectInbox = () => {\n if (destroyed) return\n try { sock = new WebSocket(wsUrl) } catch { scheduleReconnect(); return }\n sock.binaryType = 'arraybuffer'\n sock.onopen = () => {\n attempt = 0\n sock!.send(encodeFrame({ type: 'auth', token }))\n sock!.send(encodeFrame({ type: 'subscribe_inbox' }))\n // On a RECONNECT (not the first connect — the initial mount already\n // fetched), catch up on anything that changed while the socket was down.\n if (connectedBefore) debouncedRefresh()\n connectedBefore = true\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data as ArrayBuffer))\n // Any inbox change for this guest → refresh the list. `new` (a freshly\n // created thread) and `update` (new message / state) both apply.\n if (frame && frame.type === 'inbox_event') debouncedRefresh()\n }\n sock.onclose = () => { sock = null; scheduleReconnect() }\n sock.onerror = () => { try { sock?.close() } catch { /* noop */ } }\n }\n const scheduleReconnect = () => {\n if (destroyed || reconnectTimer) return\n const delay = Math.min(15_000, 500 * 2 ** attempt++) + Math.random() * 250\n reconnectTimer = setTimeout(() => { reconnectTimer = undefined; connectInbox() }, delay)\n }\n connectInbox()\n\n return {\n refresh,\n close() {\n destroyed = true\n if (reconnectTimer) clearTimeout(reconnectTimer)\n if (refreshTimer) clearTimeout(refreshTimer)\n compactObserver?.disconnect()\n compactObserver = null\n try { sock?.close() } catch { /* noop */ }\n sock = null\n opts.el.replaceChildren()\n },\n defaultProfileId() { return opts.profileId ?? serverDefaultProfileId },\n }\n}\n"],"names":["timeAgo","ts","s","days","CHAT_ICON_SVG","el","tag","cls","text","e","CSS","mountChatList","opts","token","persistentUid","httpBase","wsUrl","resolveRelayUrls","i18n","accent","root","head","titleEl","compose","searchWrap","searchIn","body","applyCompact","w","compactObserver","entries","_a","serverDefaultProfileId","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","who","url","headers","res","data","a","b","renderRows","query","filtered","rowName","empty","start","isUnread","unread","read","buildRow","entry","name","initial","lastSeq","unreadCount","row","av","dot","info","stateMap","right","refresh","errBox","retry","sock","reconnectTimer","refreshTimer","attempt","connectedBefore","debouncedRefresh","connectInbox","scheduleReconnect","encodeFrame","ev","frame","decodeFrame","delay"],"mappings":";AAqHA,SAASA,EAAQC,GAAoB;AACnC,QAAMC,IAAI,KAAK,OAAO,KAAK,IAAA,IAAQD,KAAM,GAAI;AAC7C,MAAIC,IAAI,GAAI,QAAO;AACnB,MAAIA,IAAI,KAAM,QAAO,GAAG,KAAK,MAAMA,IAAI,EAAE,CAAC;AAC1C,MAAIA,IAAI,MAAO,QAAO,GAAG,KAAK,MAAMA,IAAI,IAAI,CAAC;AAC7C,QAAMC,IAAO,KAAK,MAAMD,IAAI,KAAK;AACjC,MAAIC,KAAQ,EAAG,QAAO,GAAGA,CAAI;AAE7B,MAAI;AAAE,WAAO,IAAI,KAAKF,CAAE,EAAE,mBAAmB,QAAW,EAAE,OAAO,SAAS,KAAK,UAAA,CAAW;AAAA,EAAE,QACtF;AAAE,WAAO,GAAGE,CAAI;AAAA,EAAI;AAC5B;AAKA,MAAMC,IACJ;AAIF,SAASC,EAAGC,GAAaC,GAAcC,GAA4B;AACjE,QAAMC,IAAI,SAAS,cAAcH,CAAG;AACpC,SAAIC,QAAO,YAAYA,IACnBC,MAAS,WAAWC,EAAE,cAAcD,IACjCC;AACT;AAEA,MAAMC,KAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CL,SAASC,GAAcC,GAAuC;AACnE,QAAMC,IAAQD,EAAK,SAASA,EAAK,UAAUE,EAAA,GACrC,EAAE,UAAAC,GAAU,OAAAC,MAAUC,EAAiBL,EAAK,KAAKA,EAAK,MAAM,GAC5DM,IAAON,EAAK,QAAQ,CAAA,GACpBO,IAASP,EAAK,UAAU;AAU9B,MAAI,CAAC,SAAS,eAAe,YAAY,GAAG;AAC1C,UAAMV,IAAI,SAAS,cAAc,OAAO;AAAG,IAAAA,EAAE,KAAK,cAClDA,EAAE,cAAcQ,IAChB,SAAS,KAAK,OAAOR,CAAC;AAAA,EACxB;AAGA,QAAMkB,IAAOf,EAAG,OAAO,KAAK;AAC5B,EAAAe,EAAK,MAAM,YAAY,gBAAgBD,CAAM,GACzCP,EAAK,qBAAmBQ,EAAK,UAAU,IAAI,eAAe;AAC9D,QAAMC,IAAOhB,EAAG,OAAO,UAAU,GAG3BiB,IAAUjB,EAAG,QAAQ,WAAW;AAKtC,MAJAiB,EAAQ,aAAa,QAAQ,KAAK,GAClCA,EAAQ,aAAa,cAAcJ,EAAK,SAAS,UAAU,GAC3DI,EAAQ,YAAYlB,GACpBiB,EAAK,OAAOC,CAAO,GACfV,EAAK,WAAW;AAClB,UAAMW,IAAUlB,EAAG,UAAU,eAAe,GAAG;AAC/C,IAAAkB,EAAQ,QAAQL,EAAK,WAAW,oBAChCK,EAAQ,iBAAiB,SAAS,MAAMX,EAAK,WAAY,GACzDS,EAAK,OAAOE,CAAO;AAAA,EACrB;AAEA,QAAMC,IAAanB,EAAG,OAAO,iBAAiB,GACxCoB,IAAWpB,EAAG,SAAS,YAAY;AACzC,EAAAoB,EAAS,cAAcP,EAAK,UAAU,cAAcO,EAAS,OAAO,UACpED,EAAW,OAAOC,CAAQ;AAE1B,QAAMC,IAAOrB,EAAG,OAAO,UAAU;AACjC,EAAAqB,EAAK,OAAOrB,EAAG,OAAO,eAAe,UAAU,CAAC,GAChDe,EAAK,OAAOC,GAAMG,GAAYE,CAAI,GAClCd,EAAK,GAAG,gBAAgBQ,CAAI;AAI5B,QAAMO,IAAe,CAACC,MAAoB;AAAE,IAAAR,EAAK,UAAU,OAAO,eAAeQ,IAAI,KAAKA,IAAI,GAAG;AAAA,EAAE;AACnG,EAAAD,EAAaP,EAAK,WAAW;AAC7B,MAAIS,IAAyC;AAS7C,MARI,OAAO,iBAAmB,QAC5BA,IAAkB,IAAI,eAAe,CAACC,MAAA;;AAAY,WAAAH,IAAaI,IAAAD,EAAQ,CAAC,MAAT,gBAAAC,EAAY,YAAY,UAASX,EAAK,WAAW;AAAA,GAAC,GACjHS,EAAgB,QAAQT,CAAI,IAM1B,CAACR,EAAK,aAAa,CAACA,EAAK,SAAU,OAAM,IAAI,MAAM,gDAAgD;AACvG,MAAIoB;AACJ,QAAMC,IAAU,YAAYrB,EAAK,aAAa,KAAKA,EAAK,QAAQ,EAAE,KAAKA,EAAK,UAAUC,GAAO,MAAM,EAAE,CAAC;AACtG,MAAIqB,IAAkC,CAAA;AACtC,MAAI;AAAE,IAAAA,IAAU,KAAK,MAAM,aAAa,QAAQD,CAAO,KAAK,IAAI;AAAA,EAAE,QAAQ;AAAA,EAAC;AAE3E,QAAME,IAAc,MAAM;AACxB,QAAI;AAAE,mBAAa,QAAQF,GAAS,KAAK,UAAUC,CAAO,CAAC;AAAA,IAAE,QAAQ;AAAA,IAAC;AAAA,EACxE;AAEA,MAAIE,IAA8B,CAAA,GAC9BC,IAAY;AAGhB,QAAMC,IAAe,YAAsC;AACzD,UAAMC,IAAM3B,EAAK,YACb,aAAa,mBAAmBA,EAAK,SAAS,CAAC,GAAGA,EAAK,UAAU,WAAW,kBAAkB,EAAE,KAChG,YAAY,mBAAmBA,EAAK,QAAS,CAAC,IAC5C4B,IAAM,GAAGzB,CAAQ,uBAAuBwB,CAAG,IAC3CE,IAAU,EAAE,eAAe,UAAU5B,CAAK,GAAA;AAMhD,QAAI6B;AACJ,QAAI;AACF,MAAAA,IAAM,MAAM,MAAMF,GAAK,EAAE,SAAAC,GAAS;AAAA,IACpC,QAAQ;AACN,MAAAC,IAAM,MAAM,MAAMF,GAAK,EAAE,SAAAC,GAAS;AAAA,IACpC;AACA,QAAI,CAACC,EAAI,GAAI,QAAO,CAAA;AACpB,UAAMC,IAAO,MAAMD,EAAI,KAAA;AACvB,WAAIC,EAAK,qBAAkBX,IAAyBW,EAAK,oBACjDA,EAAK,iBAAiB,CAAA,GAAI,KAAK,CAACC,GAAGC,MAAMA,EAAE,YAAYD,EAAE,SAAS;AAAA,EAC5E,GAGME,IAAa,CAAChB,GAA0BiB,MAAkB;AAC9D,QAAIV,EAAW;AACf,UAAMW,IAAWD,IACbjB,EAAQ;AAAA,MAAO,CAAArB,MACbwC,EAAQxC,CAAC,EAAE,cAAc,SAASsC,CAAK,MACtCtC,EAAE,eAAe,IAAI,YAAA,EAAc,SAASsC,CAAK;AAAA,IAAA,IAEpDjB;AAIJ,QAFAJ,EAAK,gBAAA,GAED,CAACsB,EAAS,QAAQ;AACpB,YAAME,IAAQ7C,EAAG,OAAO,aAAa0C,IAAQ,gBAAiB7B,EAAK,SAAS,uBAAwB;AACpG,UAAI,CAAC6B,KAASnC,EAAK,WAAW;AAC5B,QAAAsC,EAAM,OAAO7C,EAAG,IAAI,CAAC;AACrB,cAAM8C,IAAQ9C,EAAG,UAAU,aAAaa,EAAK,WAAW,sBAAsB;AAC9E,QAAAiC,EAAM,iBAAiB,SAAS,MAAMvC,EAAK,WAAY,GACvDsC,EAAM,OAAOC,CAAK;AAAA,MACpB;AACA,MAAAzB,EAAK,OAAOwB,CAAK;AACjB;AAAA,IACF;AAEA,UAAME,IAAW,CAAC3C,OACfA,EAAE,WAAW,MAAMyB,EAAQzB,EAAE,EAAE,KAAK,IAEjC4C,IAASL,EAAS,OAAOI,CAAQ,GACjCE,IAASN,EAAS,OAAO,OAAK,CAACI,EAAS3C,CAAC,CAAC;AAEhD,QAAI4C,EAAO,QAAQ;AACjB,MAAA3B,EAAK,OAAOrB,EAAG,OAAO,eAAe,GAAGa,EAAK,UAAU,QAAQ,KAAKmC,EAAO,MAAM,GAAG,CAAC;AACrF,iBAAW5C,KAAK4C,EAAQ,CAAA3B,EAAK,OAAO6B,EAAS9C,GAAG2C,EAAS3C,CAAC,CAAC,CAAC;AAAA,IAC9D;AACA,QAAI6C,EAAK,QAAQ;AACf,MAAA5B,EAAK,OAAOrB,EAAG,OAAO,eAAegD,EAAO,SAAUnC,EAAK,OAAO,sBAAuB,EAAE,CAAC;AAC5F,iBAAWT,KAAK6C,EAAM,CAAA5B,EAAK,OAAO6B,EAAS9C,GAAG,EAAK,CAAC;AAAA,IACtD;AAAA,EACF,GAEMwC,IAAU,CAACO,MACfA,EAAM,iBAAiBA,EAAM,SAAS,WAAYA,EAAM,UAAU,mBAAoB,oBAElFD,IAAW,CAACC,GAAsBH,MAAiC;;AACvE,UAAMI,IAAOR,EAAQO,CAAK,GAIpBE,OAAW3B,IAAA0B,EAAK,MAAM,WAAA,UAAA,GAAQ,OAAnB,gBAAA1B,EAAuB,OAAM0B,EAAK,KAAA,EAAO,CAAC,KAAK,KAAK,YAAA,GAC/DE,IAAUH,EAAM,WAAW,GAC3BI,IAAcP,IAAS,KAAK,IAAI,GAAGM,KAAWzB,EAAQsB,EAAM,EAAE,KAAK,EAAE,IAAI,GAEzEK,IAAMxD,EAAG,UAAU,UAAUgD,IAAS,YAAY,EAAE,EAAE,GAGtDS,IAAKzD,EAAG,OAAO,UAAUqD,CAAO,GAChCK,IAAM1D,EAAG,OAAO,SAAS;AAC/B,IAAImD,EAAM,UAAU,SAAQO,EAAI,UAAU,IAAI,MAAM,IAC3CP,EAAM,UAAU,oBAAkBO,EAAI,UAAU,IAAI,SAAS,GACtED,EAAG,OAAOC,CAAG,GACbF,EAAI,OAAOC,CAAE;AAGb,UAAME,IAAO3D,EAAG,OAAO,UAAU;AACjC,IAAA2D,EAAK,OAAO3D,EAAG,OAAO,YAAYoD,CAAI,CAAC;AACvC,UAAMQ,IAAmC;AAAA,MACvC,MAAM;AAAA,MAAQ,gBAAgB;AAAA,MAC9B,UAAU;AAAA,MAAc,QAAQ;AAAA,IAAA;AAElC,IAAAD,EAAK,OAAO3D,EAAG,OAAO,eAAemD,EAAM,eAAeS,EAAST,EAAM,KAAK,KAAKA,EAAM,KAAK,CAAC,GAC/FK,EAAI,OAAOG,CAAI;AAGf,UAAME,IAAQ7D,EAAG,OAAO,WAAW;AACnC,WAAA6D,EAAM,OAAO7D,EAAG,OAAO,YAAYL,EAAQwD,EAAM,SAAS,CAAC,CAAC,GACxDI,IAAc,KAChBM,EAAM,OAAO7D,EAAG,OAAO,aAAa,OAAOuD,IAAc,KAAK,QAAQA,CAAW,CAAC,CAAC,GAErFC,EAAI,OAAOK,CAAK,GAEhBL,EAAI,iBAAiB,SAAS,MAAM;;AAElC,MAAIF,IAAU,MAAKzB,EAAQsB,EAAM,EAAE,IAAIG,GAASxB,EAAA,IAChD0B,EAAI,UAAU,OAAO,QAAQ,IAC7B9B,IAAAmC,EAAM,cAAc,YAAY,MAAhC,QAAAnC,EAAmC,UACnCnB,EAAK,SAAS4C,CAAK;AAAA,IACrB,CAAC,GAEMK;AAAA,EACT,GAEMM,IAAU,MAAM;AACpB,IAAI9B,KACJC,EAAA,EAAe,KAAK,CAAAR,MAAW;AAC7B,MAAIO,MACJD,IAAaN,GACbgB,EAAWhB,GAASL,EAAS,MAAM,KAAA,EAAO,aAAa;AAAA,IACzD,CAAC,EAAE,MAAM,CAAC,MAAM;AACd,UAAIY,EAAW;AACf,cAAQ,MAAM,mDAAmDtB,CAAQ,uDAAuD,CAAC;AACjI,YAAMqD,IAAS/D,EAAG,OAAO,aAAaa,EAAK,SAAS,+BAA+B;AACnF,MAAAkD,EAAO,OAAO/D,EAAG,IAAI,CAAC;AACtB,YAAMgE,IAAQhE,EAAG,UAAU,aAAaa,EAAK,SAAS,OAAO;AAC7D,MAAAmD,EAAM,iBAAiB,SAAS,MAAM;AACpC,QAAA3C,EAAK,gBAAgBrB,EAAG,OAAO,eAAe,UAAU,CAAC,GACzD8D,EAAA;AAAA,MACF,CAAC,GACDC,EAAO,OAAOC,CAAK,GACnB3C,EAAK,gBAAgB0C,CAAM;AAAA,IAC7B,CAAC;AAAA,EACH;AAEA,EAAA3C,EAAS,iBAAiB,SAAS,MAAMqB,EAAWV,GAAYX,EAAS,MAAM,OAAO,YAAA,CAAa,CAAC,GAGpG0C,EAAA;AAOA,MAAIG,IAAyB,MACzBC,GACAC,GACAC,IAAU,GACVC,IAAkB;AACtB,QAAMC,IAAmB,MAAM;AAC7B,IAAIH,MACJA,IAAe,WAAW,MAAM;AAAE,MAAAA,IAAe,QAAWL,EAAA;AAAA,IAAU,GAAG,GAAG;AAAA,EAC9E,GACMS,IAAe,MAAM;AACzB,QAAI,CAAAvC,GACJ;AAAA,UAAI;AAAE,QAAAiC,IAAO,IAAI,UAAUtD,CAAK;AAAA,MAAE,QAAQ;AAAE,QAAA6D,EAAA;AAAqB;AAAA,MAAO;AACxE,MAAAP,EAAK,aAAa,eAClBA,EAAK,SAAS,MAAM;AAClB,QAAAG,IAAU,GACVH,EAAM,KAAKQ,EAAY,EAAE,MAAM,QAAQ,OAAAjE,EAAA,CAAO,CAAC,GAC/CyD,EAAM,KAAKQ,EAAY,EAAE,MAAM,kBAAA,CAAmB,CAAC,GAG/CJ,KAAiBC,EAAA,GACrBD,IAAkB;AAAA,MACpB,GACAJ,EAAK,YAAY,CAACS,MAAO;AACvB,cAAMC,IAAQC,EAAY,IAAI,WAAWF,EAAG,IAAmB,CAAC;AAGhE,QAAIC,KAASA,EAAM,SAAS,iBAAeL,EAAA;AAAA,MAC7C,GACAL,EAAK,UAAU,MAAM;AAAE,QAAAA,IAAO,MAAMO,EAAA;AAAA,MAAoB,GACxDP,EAAK,UAAU,MAAM;AAAE,YAAI;AAAE,UAAAA,KAAA,QAAAA,EAAM;AAAA,QAAQ,QAAQ;AAAA,QAAa;AAAA,MAAE;AAAA;AAAA,EACpE,GACMO,IAAoB,MAAM;AAC9B,QAAIxC,KAAakC,EAAgB;AACjC,UAAMW,IAAQ,KAAK,IAAI,MAAQ,MAAM,KAAKT,GAAS,IAAI,KAAK,OAAA,IAAW;AACvE,IAAAF,IAAiB,WAAW,MAAM;AAAE,MAAAA,IAAiB,QAAWK,EAAA;AAAA,IAAe,GAAGM,CAAK;AAAA,EACzF;AACA,SAAAN,EAAA,GAEO;AAAA,IACL,SAAAT;AAAA,IACA,QAAQ;AACN,MAAA9B,IAAY,IACRkC,kBAA6BA,CAAc,GAC3CC,kBAA2BA,CAAY,GAC3C3C,KAAA,QAAAA,EAAiB,cACjBA,IAAkB;AAClB,UAAI;AAAE,QAAAyC,KAAA,QAAAA,EAAM;AAAA,MAAQ,QAAQ;AAAA,MAAa;AACzC,MAAAA,IAAO,MACP1D,EAAK,GAAG,gBAAA;AAAA,IACV;AAAA,IACA,mBAAmB;AAAE,aAAOA,EAAK,aAAaoB;AAAA,IAAuB;AAAA,EAAA;AAEzE;"}
|
|
1
|
+
{"version":3,"file":"chatlist.js","sources":["../src/chatlist.styles.ts","../src/chatlist.ts"],"sourcesContent":["// Injected stylesheet for the chat list (`.ocl`). Extracted from chatlist.ts.\n// Tokens come from theme-tokens.ts (single source of truth).\nimport { lightTokens, darkTokens } from './theme-tokens.js'\n\nexport const CSS = `\n.ocl { ${lightTokens('ocl')}\n display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);\n font-family:var(--ocl-fb); color:var(--ocl-ink); overflow:hidden; }\n@media (prefers-color-scheme: dark) { .ocl:not([data-theme=\"light\"]) { ${darkTokens('ocl')} } }\n.ocl[data-theme=\"dark\"] { ${darkTokens('ocl')} }\n.ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }\n.ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }\n.ocl-title svg { width:24px; height:24px; display:block; }\n.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 20px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n.ocl-search-wrap { padding:4px 12px 8px; background:var(--ocl-bg); }\n.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-tint); border-radius:20px; padding:9px 14px; font:inherit; font-size:14px; outline:none; }\n.ocl-body { flex:1; overflow-y:auto; }\n.ocl-section { padding:10px 18px 4px; font-family:var(--ocl-fh); font-size:11px; font-weight:600; color:var(--ocl-mut); text-transform:uppercase; letter-spacing:.5px; background:transparent; }\n.ocl-empty { padding:40px 20px; text-align:center; color:var(--ocl-mut); font-size:14px; }\n.ocl-row { display:flex; align-items:center; gap:12px; padding:10px 12px; margin:6px 12px; background:var(--ocl-tint); border:none; width:calc(100% - 24px); box-sizing:border-box; border-radius:16px; text-align:left; cursor:pointer; transition:background .12s; }\n.ocl-row:hover { background:var(--ocl-rowhover); }\n.ocl-row.unread { background:var(--ocl-rowhover); }\n.ocl-av { width:40px; height:40px; border-radius:50%; background:var(--ocl-accent); color:var(--ocl-onaccent); font-family:var(--ocl-fh); font-size:15px; font-weight:600; display:flex; align-items:center; justify-content:center; flex:none; }\n.ocl-info { flex:1; min-width:0; }\n.ocl-name { font-family:var(--ocl-fh); font-size:14px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }\n.ocl-row.unread .ocl-name { font-weight:700; }\n.ocl-preview { font-size:11.5px; color:var(--ocl-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.ocl-row.unread .ocl-preview { color:var(--ocl-ink); }\n.ocl-right { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex:none; }\n.ocl-time { font-size:10px; color:var(--ocl-mut); }\n.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }\n.ocl-badge { background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:999px; font-size:11px; font-weight:700; min-width:20px; height:20px; padding:0 5px; display:flex; align-items:center; justify-content:center; }\n.ocl-status { font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }\n.ocl-status.open { background:#dff3e6; color:#2f8a52; }\n.ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }\n.ocl-status.done { background:#eee; color:#777; }\n.ocl[data-theme=\"dark\"] .ocl-status.done { background:#39325e; color:#b3abd6; }\n@media (prefers-color-scheme: dark) { .ocl:not([data-theme=\"light\"]) .ocl-status.done { background:#39325e; color:#b3abd6; } }\n.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }\n.ocl-compose { border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); width:32px; height:32px; border-radius:50%; font-size:18px; line-height:1; cursor:pointer; box-shadow:var(--ocl-shadow); }\n.ocl-compose:hover { filter:brightness(1.08); }\n/* When the HOST overlays a ✕ (ChatApp's onClose / embed's inbox stack), it is\n absolutely positioned at top-right and used to land straight on top of the ✎\n compose button. Reserve the space instead of stacking them. */\n.ocl-has-close .ocl-head { padding-right:52px; }\n.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n/* Compact sizing keyed on the LIST'S OWN width (ResizeObserver toggles\n * .ocl-compact below 400px) — covers phones and narrow desktop embeds alike.\n * Search must be ≥16px in compact or iOS zooms the page on focus. */\n.ocl-compact .ocl-row { padding:13px 14px; }\n.ocl-compact .ocl-search { font-size:16px; }\n.ocl-compact .ocl-compose { width:36px; height:36px; }\n.ocl-row:focus-visible, .ocl button:focus-visible, .ocl input:focus-visible { outline:2px solid var(--ocl-accent); outline-offset:2px; border-radius:12px; }\n`\n","/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: 'https://api.relay.paramms.com', // ONE url, any scheme\n * profileId: 'p_usedcars',\n * userId: currentUser.id, // optional — uses localStorage UID if omitted\n * onSelect: (entry) => {\n * window.location.href = `/listings/${entry.subjectId}#chat`\n * },\n * })\n * handle.refresh() // manually re-fetch the list\n * handle.close() // unmount and clean up\n */\n\nimport { resolveRelayUrls } from './history.js'\nimport { CSS } from './chatlist.styles.js'\nimport { persistentUid } from './uid.js'\nimport { encodeFrame, decodeFrame } from './protocol/codec.js'\n\nexport interface ChatListEntry {\n id: string\n /** Chatroom this conversation belongs to. With `scope: 'tenant'` this can\n * differ from the profileId the list was mounted with — open the chat\n * against THIS profileId. */\n profileId?: string\n /** 'support' (default) or 'direct' (user↔user DM). */\n kind?: string\n /** For direct conversations: the other participant's user id. */\n peerId?: string\n subjectId?: string\n subjectTitle?: string\n /** One-line detail — e.g. \"45,000 km · Auto\" */\n subjectMeta?: string\n /** URL of the listing/item page — stored automatically when the widget first opens */\n subjectUrl?: string\n state: string\n updatedAt: number\n lastSeq?: number\n lastMessage?: string\n}\n\nexport interface ChatListOptions {\n /** Mount target element */\n el: HTMLElement\n /** Relay URL — ONE url, any scheme (https recommended). The WebSocket URL\n * and REST base are derived automatically. */\n url: string\n /** HTTP(S) base for REST — only when REST is on a different origin.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Profile ID to scope conversations to */\n profileId?: string\n /** Tenant-level identification — list EVERY conversation this user has\n * with the business, across ALL of its chatrooms, without naming one\n * (e.g. a platform running a marketplace chatroom AND a general-support\n * chatroom). Provide `profileId` OR `tenantId` (profileId wins if both;\n * it also fixes where \"new conversation\" opens). With only `tenantId`,\n * the compose target is the server-reported defaultProfileId (the\n * tenant's oldest chatroom). */\n tenantId?: string\n /** A signed identity token (ES256 JWT) — the production identity tier for\n * chatrooms with signed identity enabled. Wins over `userId`. */\n token?: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Which conversations to list (default 'profile'):\n * 'profile' — only this chatroom's threads.\n * 'tenant' — every conversation this user has with the chatroom's owning\n * business, across ALL of its chatrooms (a real chat-app inbox). Rows\n * carry `profileId` so each opens against the right chatroom. */\n scope?: 'profile' | 'tenant'\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** When provided, the list shows a ✎ compose button in the header (and a\n * \"Start a conversation\" button in the empty state) that calls this —\n * wire it to open a fresh/general thread. Without it a user with no\n * conversations yet has nothing to tap. */\n onNewChat?: () => void\n /** Set when the CALLER draws its own close (✕) control overlaying the list's\n * top-right corner — reserves header space so it doesn't sit on top of the\n * ✎ compose button. */\n reserveCloseSpace?: boolean\n /** Brand colour hex — default '#6c5ce7' */\n accent?: string\n theme?: 'auto' | 'light' | 'dark'\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n retry?: string // default 'Retry'\n close?: string // default 'Close' (aria-label for the ✕ control)\n newChat?: string // default 'New conversation' / 'Start a conversation'\n }\n}\n\nexport interface ChatListHandle {\n /** Re-fetch and re-render the list */\n refresh(): void\n /** Unmount and clean up */\n close(): void\n /** Where \"new conversation\" should open: the configured profileId, else the\n * server-reported tenant default (oldest chatroom). Undefined until the\n * first successful fetch when only tenantId was configured. */\n defaultProfileId(): string | undefined\n}\n\n/** Map a conversation state to a status chip (label + style class). Returns null\n * for states with no meaningful badge. */\nfunction statusChip(state: string): { label: string; cls: string } | null {\n switch (state) {\n case 'open': return { label: 'Open', cls: 'open' }\n case 'awaiting_staff':return { label: 'Waiting on you', cls: 'waiting' }\n case 'resolved': return { label: 'Resolved', cls: 'done' }\n case 'closed': return { label: 'Closed', cls: 'done' }\n default: return null\n }\n}\n\nfunction timeAgo(ts: number): string {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return 'just now'\n if (s < 3600) return `${Math.floor(s / 60)}m`\n if (s < 86400) return `${Math.floor(s / 3600)}h`\n const days = Math.floor(s / 86400)\n if (days <= 7) return `${days}d`\n // Beyond a week, a date scans better than \"43d\" (what Channel.io/Intercom do).\n try { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) }\n catch { return `${days}d` }\n}\n\n// Inline, dependency-free chat-bubble glyph. `currentColor` picks up the\n// header's accent tint. No external font/emoji so it renders identically\n// across platforms (the old '💬'/text wordmark varied per-OS).\nconst CHAT_ICON_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\" ' +\n 'fill=\"currentColor\" fill-opacity=\"0.12\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'\n\nfunction el(tag: string, cls?: string, text?: string): HTMLElement {\n const e = document.createElement(tag)\n if (cls) e.className = cls\n if (text !== undefined) e.textContent = text\n return e\n}\n\n// CSS lives in chatlist.styles.ts (single source of truth for .ocl styling).\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.token ?? opts.userId ?? persistentUid()\n const { httpBase, wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#6c5ce7'\n\n // Inject the shared stylesheet ONCE, and keep it accent-FREE. The accent was\n // previously baked into this shared <style> (CSS.replace(/#f5713c/g, accent)),\n // which meant the FIRST list mounted on a page won the accent for EVERY list\n // after it (the style tag already existed, so a second list's colour was\n // ignored) — real interference when a project runs more than one widget. The\n // accent now lives in a per-instance CSS variable set on the root element\n // below, so each list keeps its own colour and the shared sheet stays static\n // (also friendlier to HTTP caching).\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS\n document.head.append(s)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n root.style.setProperty('--ocl-accent', accent) // per-instance accent\n if (opts.theme && opts.theme !== 'auto') root.dataset.theme = opts.theme\n if (opts.reserveCloseSpace) root.classList.add('ocl-has-close')\n const head = el('div', 'ocl-head')\n // Header shows a chat icon (accent-tinted) rather than a \"Messages\" wordmark.\n // The i18n title is still exposed as the accessible label for screen readers.\n const titleEl = el('span', 'ocl-title')\n titleEl.setAttribute('role', 'img')\n titleEl.setAttribute('aria-label', i18n.title ?? 'Messages')\n titleEl.innerHTML = CHAT_ICON_SVG\n head.append(titleEl)\n if (opts.onNewChat) {\n const compose = el('button', 'ocl-compose', '✎') as HTMLButtonElement\n compose.title = i18n.newChat ?? 'New conversation'\n compose.addEventListener('click', () => opts.onNewChat!())\n head.append(compose)\n }\n\n const searchWrap = el('div', 'ocl-search-wrap')\n const searchIn = el('input', 'ocl-search') as HTMLInputElement\n searchIn.placeholder = i18n.search ?? '🔍 Search'; searchIn.type = 'search'\n searchWrap.append(searchIn)\n\n const body = el('div', 'ocl-body')\n body.append(el('div', 'ocl-spinner', 'Loading…'))\n root.append(head, searchWrap, body)\n opts.el.replaceChildren(root)\n\n // Container-driven compact sizing (see the CSS note). Guarded for\n // jsdom/old runtimes without ResizeObserver — they keep desktop sizing.\n const applyCompact = (w: number): void => { root.classList.toggle('ocl-compact', w > 0 && w < 400) }\n applyCompact(root.clientWidth)\n let compactObserver: ResizeObserver | null = null\n if (typeof ResizeObserver !== 'undefined') {\n compactObserver = new ResizeObserver((entries) => applyCompact(entries[0]?.contentRect.width ?? root.clientWidth))\n compactObserver.observe(root)\n }\n\n // Track seen seqs for unread counts (persisted in localStorage)\n // Key unread tracking by the STABLE id (userId beats token here: a signed\n // JWT changes every mint, which would reset unread counts on each load).\n if (!opts.profileId && !opts.tenantId) throw new Error('[relay chatlist] provide profileId or tenantId')\n let serverDefaultProfileId: string | undefined\n const seenKey = `ocl_seen_${opts.profileId ?? `t_${opts.tenantId}`}_${(opts.userId ?? token).slice(-8)}`\n let seenSeq: Record<string, number> = {}\n try { seenSeq = JSON.parse(localStorage.getItem(seenKey) ?? '{}') } catch {}\n\n const saveSeenSeq = () => {\n try { localStorage.setItem(seenKey, JSON.stringify(seenSeq)) } catch {}\n }\n\n let allEntries: ChatListEntry[] = []\n let destroyed = false\n\n // Fetch conversations from server\n const fetchEntries = async (): Promise<ChatListEntry[]> => {\n const who = opts.profileId\n ? `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n : `tenantId=${encodeURIComponent(opts.tenantId!)}` // tenant-level is inherently tenant-scoped\n const url = `${httpBase}/conversations/mine?${who}`\n const headers = { authorization: `Bearer ${token}` }\n // A GET is safe to repeat. A single transient network reject — the socket\n // still warming up right after a reload, a relay that blipped — used to\n // dead-end straight to \"Could not load conversations.\" Retry once so a blip\n // self-heals; an HTTP error (4xx/5xx) is NOT a network failure and returns\n // an empty list rather than retrying or erroring.\n let res: Response\n try {\n res = await fetch(url, { headers })\n } catch {\n res = await fetch(url, { headers }) // one immediate retry\n }\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[]; defaultProfileId?: string }\n if (data.defaultProfileId) serverDefaultProfileId = data.defaultProfileId\n return (data.conversations ?? []).sort((a, b) => b.updatedAt - a.updatedAt)\n }\n\n // Render rows from entries, optionally filtered by search query\n const renderRows = (entries: ChatListEntry[], query: string) => {\n if (destroyed) return\n const filtered = query\n ? entries.filter(e =>\n rowName(e).toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n const empty = el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.'))\n if (!query && opts.onNewChat) {\n empty.append(el('br'))\n const start = el('button', 'ocl-start', i18n.newChat ?? 'Start a conversation') as HTMLButtonElement\n start.addEventListener('click', () => opts.onNewChat!())\n empty.append(start)\n }\n body.append(empty)\n return\n }\n\n const isUnread = (e: ChatListEntry) =>\n (e.lastSeq ?? 0) > (seenSeq[e.id] ?? 0)\n\n const unread = filtered.filter(isUnread)\n const read = filtered.filter(e => !isUnread(e))\n\n if (unread.length) {\n body.append(el('div', 'ocl-section', `${i18n.unread ?? 'Unread'} (${unread.length})`))\n for (const e of unread) body.append(buildRow(e, isUnread(e)))\n }\n if (read.length) {\n body.append(el('div', 'ocl-section', unread.length ? (i18n.all ?? 'All conversations') : ''))\n for (const e of read) body.append(buildRow(e, false))\n }\n }\n\n const rowName = (entry: ChatListEntry): string =>\n entry.subjectTitle ?? (entry.kind === 'direct' ? (entry.peerId ?? 'Direct message') : 'General enquiry')\n\n const buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = rowName(entry)\n // Prefer the first LETTER (any script), not a leading digit/symbol — a\n // listing titled \"2018 Kia K7\" should show \"K\", not \"2\", and Korean/other\n // scripts pick their first character too.\n const initial = (name.match(/\\p{L}/u)?.[0] ?? name.trim()[0] ?? '?').toUpperCase()\n const lastSeq = entry.lastSeq ?? 0\n const unreadCount = unread ? Math.max(1, lastSeq - (seenSeq[entry.id] ?? 0)) : 0\n\n const row = el('button', `ocl-row${unread ? ' unread' : ''}`) as HTMLButtonElement\n\n // Avatar\n const av = el('div', 'ocl-av', initial)\n row.append(av)\n\n // Info\n const info = el('div', 'ocl-info')\n info.append(el('div', 'ocl-name', name))\n const stateMap: Record<string, string> = {\n open: 'Open', awaiting_staff: 'Waiting for reply…',\n resolved: 'Resolved ✓', closed: 'Closed',\n }\n info.append(el('div', 'ocl-preview', entry.lastMessage ?? stateMap[entry.state] ?? entry.state))\n row.append(info)\n\n // Right: timestamp, status chip, unread badge\n const right = el('div', 'ocl-right')\n right.append(el('div', 'ocl-time', timeAgo(entry.updatedAt)))\n const chip = statusChip(entry.state)\n if (chip) right.append(el('div', `ocl-status ${chip.cls}`, chip.label))\n if (unreadCount > 0) {\n right.append(el('div', 'ocl-badge', String(unreadCount > 99 ? '99+' : unreadCount)))\n }\n row.append(right)\n\n row.addEventListener('click', () => {\n // Mark as read\n if (lastSeq > 0) { seenSeq[entry.id] = lastSeq; saveSeenSeq() }\n row.classList.remove('unread')\n right.querySelector('.ocl-badge')?.remove()\n opts.onSelect(entry)\n })\n\n return row\n }\n\n const refresh = () => {\n if (destroyed) return\n fetchEntries().then(entries => {\n if (destroyed) return\n allEntries = entries\n renderRows(entries, searchIn.value.trim().toLowerCase())\n }).catch((e) => {\n if (destroyed) return\n console.error(`[chat-widget] failed to load conversations from ${httpBase}/conversations/mine — check the apiUrl/CORS config.`, e)\n const errBox = el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.')\n errBox.append(el('br'))\n const retry = el('button', 'ocl-retry', i18n.retry ?? 'Retry') as HTMLButtonElement\n retry.addEventListener('click', () => {\n body.replaceChildren(el('div', 'ocl-spinner', 'Loading…'))\n refresh()\n })\n errBox.append(retry)\n body.replaceChildren(errBox)\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n // ── Live inbox: subscribe over WS so the list updates the instant any of the\n // guest's conversations changes, instead of only on the periodic poll. The\n // server streams `inbox_event` to the guest's OWN inbox (keyed by guestId).\n // We coalesce bursts and re-fetch (the fetch already sorts/dedupes); the poll\n // stays as a backstop for a dropped socket. ────────────────────────────────\n let sock: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let refreshTimer: ReturnType<typeof setTimeout> | undefined\n let attempt = 0\n let connectedBefore = false\n const debouncedRefresh = () => {\n if (refreshTimer) return\n refreshTimer = setTimeout(() => { refreshTimer = undefined; refresh() }, 300)\n }\n const connectInbox = () => {\n if (destroyed) return\n try { sock = new WebSocket(wsUrl) } catch { scheduleReconnect(); return }\n sock.binaryType = 'arraybuffer'\n sock.onopen = () => {\n attempt = 0\n sock!.send(encodeFrame({ type: 'auth', token }))\n sock!.send(encodeFrame({ type: 'subscribe_inbox' }))\n // On a RECONNECT (not the first connect — the initial mount already\n // fetched), catch up on anything that changed while the socket was down.\n if (connectedBefore) debouncedRefresh()\n connectedBefore = true\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data as ArrayBuffer))\n // Any inbox change for this guest → refresh the list. `new` (a freshly\n // created thread) and `update` (new message / state) both apply.\n if (frame && frame.type === 'inbox_event') debouncedRefresh()\n }\n sock.onclose = () => { sock = null; scheduleReconnect() }\n sock.onerror = () => { try { sock?.close() } catch { /* noop */ } }\n }\n const scheduleReconnect = () => {\n if (destroyed || reconnectTimer) return\n const delay = Math.min(15_000, 500 * 2 ** attempt++) + Math.random() * 250\n reconnectTimer = setTimeout(() => { reconnectTimer = undefined; connectInbox() }, delay)\n }\n connectInbox()\n\n return {\n refresh,\n close() {\n destroyed = true\n if (reconnectTimer) clearTimeout(reconnectTimer)\n if (refreshTimer) clearTimeout(refreshTimer)\n compactObserver?.disconnect()\n compactObserver = null\n try { sock?.close() } catch { /* noop */ }\n sock = null\n opts.el.replaceChildren()\n },\n defaultProfileId() { return opts.profileId ?? serverDefaultProfileId },\n }\n}\n"],"names":["CSS","lightTokens","darkTokens","statusChip","state","timeAgo","ts","s","days","CHAT_ICON_SVG","el","tag","cls","text","e","mountChatList","opts","token","persistentUid","httpBase","wsUrl","resolveRelayUrls","i18n","accent","root","head","titleEl","compose","searchWrap","searchIn","body","applyCompact","w","compactObserver","entries","_a","serverDefaultProfileId","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","who","url","headers","res","data","a","b","renderRows","query","filtered","rowName","empty","start","isUnread","unread","read","buildRow","entry","name","initial","lastSeq","unreadCount","row","av","info","stateMap","right","chip","refresh","errBox","retry","sock","reconnectTimer","refreshTimer","attempt","connectedBefore","debouncedRefresh","connectInbox","scheduleReconnect","encodeFrame","ev","frame","decodeFrame","delay"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CSS: string;
|