@paramms/chat-widget 1.0.46 β†’ 1.0.47

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/chatlist.js CHANGED
@@ -3,7 +3,7 @@ const ee = `
3
3
  .ocl { ${Q("ocl")}
4
4
  display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);
5
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")} } }
6
+ @media (prefers-color-scheme: dark) { .ocl[data-theme="auto"] { ${H("ocl")} } }
7
7
  .ocl[data-theme="dark"] { ${H("ocl")} }
8
8
  .ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }
9
9
  .ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }
@@ -32,7 +32,7 @@ const ee = `
32
32
  .ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }
33
33
  .ocl-status.done { background:#eee; color:#777; }
34
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; } }
35
+ @media (prefers-color-scheme: dark) { .ocl[data-theme="auto"] .ocl-status.done { background:#39325e; color:#b3abd6; } }
36
36
  .ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }
37
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
38
  .ocl-compose:hover { filter:brightness(1.08); }
@@ -91,8 +91,8 @@ function ae(o) {
91
91
  const e = document.createElement("link");
92
92
  e.id = "ocw-webfont", e.rel = "stylesheet", e.href = "https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Nunito:wght@400;500;600;700&display=swap", document.head.append(e);
93
93
  }
94
- const h = t("div", "ocl");
95
- h.style.setProperty("--ocl-accent", G), o.theme && o.theme !== "auto" && (h.dataset.theme = o.theme), o.reserveCloseSpace && h.classList.add("ocl-has-close");
94
+ const u = t("div", "ocl");
95
+ u.style.setProperty("--ocl-accent", G), u.dataset.theme = o.theme ?? "light", o.reserveCloseSpace && u.classList.add("ocl-has-close");
96
96
  const z = t("div", "ocl-head"), C = t("span", "ocl-title");
97
97
  if (C.setAttribute("role", "img"), C.setAttribute("aria-label", f.title ?? "Messages"), C.innerHTML = ne, z.append(C), o.onNewChat) {
98
98
  const e = t("button", "ocl-compose", "✎");
@@ -101,16 +101,16 @@ function ae(o) {
101
101
  const R = t("div", "ocl-search-wrap"), m = t("input", "ocl-search");
102
102
  m.placeholder = f.search ?? "πŸ” Search", m.type = "search", R.append(m);
103
103
  const s = t("div", "ocl-body");
104
- s.append(t("div", "ocl-spinner", "Loading…")), h.append(z, R, s), o.el.replaceChildren(h);
104
+ s.append(t("div", "ocl-spinner", "Loading…")), u.append(z, R, s), o.el.replaceChildren(u);
105
105
  const E = (e) => {
106
- h.classList.toggle("ocl-compact", e > 0 && e < 400);
106
+ u.classList.toggle("ocl-compact", e > 0 && e < 400);
107
107
  };
108
- E(h.clientWidth);
108
+ E(u.clientWidth);
109
109
  let v = null;
110
110
  if (typeof ResizeObserver < "u" && (v = new ResizeObserver((e) => {
111
111
  var r;
112
- return E(((r = e[0]) == null ? void 0 : r.contentRect.width) ?? h.clientWidth);
113
- }), v.observe(h)), !o.profileId && !o.tenantId) throw new Error("[relay chatlist] provide profileId or tenantId");
112
+ return E(((r = e[0]) == null ? void 0 : r.contentRect.width) ?? u.clientWidth);
113
+ }), v.observe(u)), !o.profileId && !o.tenantId) throw new Error("[relay chatlist] provide profileId or tenantId");
114
114
  let N;
115
115
  const T = `ocl_seen_${o.profileId ?? `t_${o.tenantId}`}_${(o.userId ?? c).slice(-8)}`;
116
116
  let b = {};
@@ -135,7 +135,7 @@ function ae(o) {
135
135
  }
136
136
  if (!d.ok) return [];
137
137
  const i = await d.json();
138
- return i.defaultProfileId && (N = i.defaultProfileId), (i.conversations ?? []).sort((u, n) => n.updatedAt - u.updatedAt);
138
+ return i.defaultProfileId && (N = i.defaultProfileId), (i.conversations ?? []).sort((h, n) => n.updatedAt - h.updatedAt);
139
139
  }, O = (e, r) => {
140
140
  if (g) return;
141
141
  const l = r ? e.filter(
@@ -151,18 +151,18 @@ function ae(o) {
151
151
  s.append(n);
152
152
  return;
153
153
  }
154
- const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0), i = l.filter(d), u = l.filter((n) => !d(n));
154
+ const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0), i = l.filter(d), h = l.filter((n) => !d(n));
155
155
  if (i.length) {
156
156
  s.append(t("div", "ocl-section", `${f.unread ?? "Unread"} (${i.length})`));
157
157
  for (const n of i) s.append(A(n, d(n)));
158
158
  }
159
- if (u.length) {
159
+ if (h.length) {
160
160
  s.append(t("div", "ocl-section", i.length ? f.all ?? "All conversations" : ""));
161
- for (const n of u) s.append(A(n, !1));
161
+ for (const n of h) s.append(A(n, !1));
162
162
  }
163
163
  }, U = (e) => e.subjectTitle ?? (e.kind === "direct" ? e.peerId ?? "Direct message" : "General enquiry"), A = (e, r) => {
164
164
  var P;
165
- 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);
165
+ 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, h = r ? Math.max(1, i - (b[e.id] ?? 0)) : 0, n = t("button", `ocl-row${r ? " unread" : ""}`), S = t("div", "ocl-av", d);
166
166
  n.append(S);
167
167
  const $ = t("div", "ocl-info");
168
168
  $.append(t("div", "ocl-name", l));
@@ -176,7 +176,7 @@ function ae(o) {
176
176
  const k = t("div", "ocl-right");
177
177
  k.append(t("div", "ocl-time", te(e.updatedAt)));
178
178
  const L = oe(e.state);
179
- 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", () => {
179
+ return L && k.append(t("div", `ocl-status ${L.cls}`, L.label)), h > 0 && k.append(t("div", "ocl-badge", String(h > 99 ? "99+" : h))), n.append(k), n.addEventListener("click", () => {
180
180
  var q;
181
181
  i > 0 && (b[e.id] = i, J()), n.classList.remove("unread"), (q = k.querySelector(".ocl-badge")) == null || q.remove(), o.onSelect(e);
182
182
  }), n;
@@ -1 +1 @@
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 webfont?: boolean\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 // Brand webfonts (shared id with the chatroom, injected once). Opt out with webfont:false.\n if (opts.webfont !== false && typeof document !== 'undefined' && !document.getElementById('ocw-webfont')) {\n const l = document.createElement('link')\n l.id = 'ocw-webfont'; l.rel = 'stylesheet'\n l.href = 'https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Nunito:wght@400;500;600;700&display=swap'\n document.head.append(l)\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","l","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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"}
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[data-theme=\"auto\"] { ${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[data-theme=\"auto\"] .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 webfont?: boolean\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 // Brand webfonts (shared id with the chatroom, injected once). Opt out with webfont:false.\n if (opts.webfont !== false && typeof document !== 'undefined' && !document.getElementById('ocw-webfont')) {\n const l = document.createElement('link')\n l.id = 'ocw-webfont'; l.rel = 'stylesheet'\n l.href = 'https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Nunito:wght@400;500;600;700&display=swap'\n document.head.append(l)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n root.style.setProperty('--ocl-accent', accent) // per-instance accent\n // Colour scheme is opt-in, never OS-auto-detected by default (see renderer.ts):\n // unset β†’ 'light'; 'auto' follows the OS; 'dark'/'light' force it.\n root.dataset.theme = opts.theme ?? 'light'\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","l","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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"}