@paramms/chat-widget 1.0.16 → 1.0.17

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.
@@ -38,6 +38,9 @@ export interface ChatListOptions {
38
38
  el: HTMLElement;
39
39
  /** Relay WebSocket URL — e.g. wss://api.paramms.com/ws */
40
40
  url: string;
41
+ /** HTTP(S) base for the REST API. Provide when the REST API is on a different
42
+ * host/path than the WebSocket. Defaults to the WS origin with `/ws` stripped. */
43
+ apiUrl?: string;
41
44
  /** Profile ID to scope conversations to */
42
45
  profileId: string;
43
46
  /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */
package/dist/chatlist.js CHANGED
@@ -3,11 +3,11 @@ function N(n) {
3
3
  const a = Math.floor((Date.now() - n) / 1e3);
4
4
  return a < 60 ? "just now" : a < 3600 ? `${Math.floor(a / 60)}m` : a < 86400 ? `${Math.floor(a / 3600)}h` : `${Math.floor(a / 86400)}d`;
5
5
  }
6
- function o(n, a, h) {
7
- const c = document.createElement(n);
8
- return a && (c.className = a), h !== void 0 && (c.textContent = h), c;
6
+ function o(n, a, u) {
7
+ const r = document.createElement(n);
8
+ return a && (r.className = a), u !== void 0 && (r.textContent = u), r;
9
9
  }
10
- const _ = `
10
+ const R = `
11
11
  .ocl { --ocl-accent:#f5713c; --ocl-bg:#f3efe9; --ocl-card:#fff; --ocl-line:#ececec; --ocl-ink:#1c1b1a; --ocl-mut:#9b9690;
12
12
  display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);
13
13
  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; color:var(--ocl-ink); overflow:hidden; }
@@ -38,58 +38,58 @@ const _ = `
38
38
  @media (max-width:480px) { .ocl-row { padding:12px 14px; } .ocl-name { font-size:14px; } }
39
39
  `;
40
40
  function O(n) {
41
- const a = n.userId ?? A(), h = B(n.url), c = n.i18n ?? {}, E = n.accent ?? "#f5713c";
41
+ const a = n.userId ?? A(), u = n.apiUrl ? n.apiUrl.replace(/\/+$/, "") : B(n.url), r = n.i18n ?? {}, U = n.accent ?? "#f5713c";
42
42
  if (!document.getElementById("ocl-styles")) {
43
43
  const e = document.createElement("style");
44
- e.id = "ocl-styles", e.textContent = _.replace(/#f5713c/g, E), document.head.append(e);
44
+ e.id = "ocl-styles", e.textContent = R.replace(/#f5713c/g, U), document.head.append(e);
45
45
  }
46
46
  const w = o("div", "ocl"), b = o("div", "ocl-head");
47
- b.append(o("span", "ocl-title", c.title ?? "Messages"));
47
+ b.append(o("span", "ocl-title", r.title ?? "Messages"));
48
48
  const y = o("div", "ocl-search-wrap"), p = o("input", "ocl-search");
49
- p.placeholder = c.search ?? "🔍 Search", p.type = "search", y.append(p);
50
- const r = o("div", "ocl-body");
51
- r.append(o("div", "ocl-spinner", "Loading…")), w.append(b, y, r), n.el.replaceChildren(w);
49
+ p.placeholder = r.search ?? "🔍 Search", p.type = "search", y.append(p);
50
+ const c = o("div", "ocl-body");
51
+ c.append(o("div", "ocl-spinner", "Loading…")), w.append(b, y, c), n.el.replaceChildren(w);
52
52
  const k = `ocl_seen_${n.profileId}_${a.slice(-8)}`;
53
- let u = {};
53
+ let g = {};
54
54
  try {
55
- u = JSON.parse(localStorage.getItem(k) ?? "{}");
55
+ g = JSON.parse(localStorage.getItem(k) ?? "{}");
56
56
  } catch {
57
57
  }
58
- const U = () => {
58
+ const E = () => {
59
59
  try {
60
- localStorage.setItem(k, JSON.stringify(u));
60
+ localStorage.setItem(k, JSON.stringify(g));
61
61
  } catch {
62
62
  }
63
63
  };
64
- let S = [], g = !1;
64
+ let S = [], h = !1;
65
65
  const j = async () => {
66
66
  const e = await fetch(
67
- `${h}/conversations/mine?profileId=${encodeURIComponent(n.profileId)}`,
67
+ `${u}/conversations/mine?profileId=${encodeURIComponent(n.profileId)}`,
68
68
  { headers: { authorization: `Bearer ${a}` } }
69
69
  );
70
70
  return e.ok ? ((await e.json()).conversations ?? []).sort((l, s) => s.updatedAt - l.updatedAt) : [];
71
71
  }, C = (e, i) => {
72
- if (g) return;
72
+ if (h) return;
73
73
  const l = i ? e.filter(
74
74
  (t) => (t.subjectTitle ?? "General enquiry").toLowerCase().includes(i) || (t.lastMessage ?? "").toLowerCase().includes(i)
75
75
  ) : e;
76
- if (r.replaceChildren(), !l.length) {
77
- r.append(o("div", "ocl-empty", i ? "No results." : c.empty ?? "No conversations yet."));
76
+ if (c.replaceChildren(), !l.length) {
77
+ c.append(o("div", "ocl-empty", i ? "No results." : r.empty ?? "No conversations yet."));
78
78
  return;
79
79
  }
80
- const s = (t) => (t.lastSeq ?? 0) > (u[t.id] ?? 0), d = l.filter(s), f = l.filter((t) => !s(t));
80
+ const s = (t) => (t.lastSeq ?? 0) > (g[t.id] ?? 0), d = l.filter(s), f = l.filter((t) => !s(t));
81
81
  if (d.length) {
82
- r.append(o("div", "ocl-section", `${c.unread ?? "Unread"} (${d.length})`));
83
- for (const t of d) r.append(z(t, s(t)));
82
+ c.append(o("div", "ocl-section", `${r.unread ?? "Unread"} (${d.length})`));
83
+ for (const t of d) c.append(z(t, s(t)));
84
84
  }
85
85
  if (f.length) {
86
- r.append(o("div", "ocl-section", d.length ? c.all ?? "All conversations" : ""));
87
- for (const t of f) r.append(z(t, !1));
86
+ c.append(o("div", "ocl-section", d.length ? r.all ?? "All conversations" : ""));
87
+ for (const t of f) c.append(z(t, !1));
88
88
  }
89
89
  }, z = (e, i) => {
90
90
  var I;
91
- const l = e.subjectTitle ?? "General enquiry", s = ((I = l[0]) == null ? void 0 : I.toUpperCase()) ?? "?", d = e.lastSeq ?? 0, f = i ? Math.max(1, d - (u[e.id] ?? 0)) : 0, t = o("button", `ocl-row${i ? " unread" : ""}`), $ = o("div", "ocl-av", s), v = o("div", "ocl-dot");
92
- e.state === "open" ? v.classList.add("open") : e.state === "awaiting_staff" && v.classList.add("waiting"), $.append(v), t.append($);
91
+ const l = e.subjectTitle ?? "General enquiry", s = ((I = l[0]) == null ? void 0 : I.toUpperCase()) ?? "?", d = e.lastSeq ?? 0, f = i ? Math.max(1, d - (g[e.id] ?? 0)) : 0, t = o("button", `ocl-row${i ? " unread" : ""}`), L = o("div", "ocl-av", s), v = o("div", "ocl-dot");
92
+ e.state === "open" ? v.classList.add("open") : e.state === "awaiting_staff" && v.classList.add("waiting"), L.append(v), t.append(L);
93
93
  const m = o("div", "ocl-info");
94
94
  m.append(o("div", "ocl-name", l));
95
95
  const q = {
@@ -102,19 +102,19 @@ function O(n) {
102
102
  const x = o("div", "ocl-right");
103
103
  return x.append(o("div", "ocl-time", N(e.updatedAt))), f > 0 && x.append(o("div", "ocl-badge", String(f > 99 ? "99+" : f))), t.append(x), t.addEventListener("click", () => {
104
104
  var M;
105
- d > 0 && (u[e.id] = d, U()), t.classList.remove("unread"), (M = x.querySelector(".ocl-badge")) == null || M.remove(), n.onSelect(e);
105
+ d > 0 && (g[e.id] = d, E()), t.classList.remove("unread"), (M = x.querySelector(".ocl-badge")) == null || M.remove(), n.onSelect(e);
106
106
  }), t;
107
- }, L = () => {
108
- g || j().then((e) => {
109
- g || (S = e, C(e, p.value.trim().toLowerCase()));
110
- }).catch(() => {
111
- g || r.replaceChildren(o("div", "ocl-empty", c.error ?? "Could not load conversations."));
107
+ }, $ = () => {
108
+ h || j().then((e) => {
109
+ h || (S = e, C(e, p.value.trim().toLowerCase()));
110
+ }).catch((e) => {
111
+ h || (console.error(`[chat-widget] failed to load conversations from ${u}/conversations/mine — check the apiUrl/CORS config.`, e), c.replaceChildren(o("div", "ocl-empty", r.error ?? "Could not load conversations.")));
112
112
  });
113
113
  };
114
- return p.addEventListener("input", () => C(S, p.value.trim().toLowerCase())), L(), {
115
- refresh: L,
114
+ return p.addEventListener("input", () => C(S, p.value.trim().toLowerCase())), $(), {
115
+ refresh: $,
116
116
  close() {
117
- g = !0, n.el.replaceChildren();
117
+ h = !0, n.el.replaceChildren();
118
118
  }
119
119
  };
120
120
  }
@@ -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: 'wss://api.paramms.com/ws',\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 { httpBaseFromWsUrl } from './history.js'\nimport { persistentUid } from './uid.js'\n\nexport interface ChatListEntry {\n id: 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 WebSocket URL — e.g. wss://api.paramms.com/ws */\n url: string\n /** Profile ID to scope conversations to */\n profileId: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** Brand colour hex — default '#f5713c' */\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 }\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}\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 return `${Math.floor(s / 86400)}d`\n}\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; font-size:18px; font-weight:700; }\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@media (max-width:480px) { .ocl-row { padding:12px 14px; } .ocl-name { font-size:14px; } }\n`\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.userId ?? persistentUid()\n const httpBase = httpBaseFromWsUrl(opts.url)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#f5713c'\n\n // Inject CSS once\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS.replace(/#f5713c/g, accent)\n document.head.append(s)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n const head = el('div', 'ocl-head')\n head.append(el('span', 'ocl-title', i18n.title ?? 'Messages'))\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 // Track seen seqs for unread counts (persisted in localStorage)\n const seenKey = `ocl_seen_${opts.profileId}_${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 res = await fetch(\n `${httpBase}/conversations/mine?profileId=${encodeURIComponent(opts.profileId)}`,\n { headers: { authorization: `Bearer ${token}` } },\n )\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[] }\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 (e.subjectTitle ?? 'General enquiry').toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n body.append(el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.')))\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 buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = entry.subjectTitle ?? 'General enquiry'\n const initial = name[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(() => {\n if (destroyed) return\n body.replaceChildren(el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.'))\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n return {\n refresh,\n close() {\n destroyed = true\n opts.el.replaceChildren()\n },\n }\n}\n"],"names":["timeAgo","ts","s","el","tag","cls","text","e","CSS","mountChatList","opts","token","persistentUid","httpBase","httpBaseFromWsUrl","i18n","accent","root","head","searchWrap","searchIn","body","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","res","a","b","renderRows","entries","query","filtered","isUnread","unread","read","buildRow","entry","name","initial","_a","lastSeq","unreadCount","row","av","dot","info","stateMap","right","refresh"],"mappings":"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"}
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: 'wss://api.paramms.com/ws',\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 { httpBaseFromWsUrl } from './history.js'\nimport { persistentUid } from './uid.js'\n\nexport interface ChatListEntry {\n id: 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 WebSocket URL — e.g. wss://api.paramms.com/ws */\n url: string\n /** HTTP(S) base for the REST API. Provide when the REST API is on a different\n * host/path than the WebSocket. Defaults to the WS origin with `/ws` stripped. */\n apiUrl?: string\n /** Profile ID to scope conversations to */\n profileId: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** Brand colour hex — default '#f5713c' */\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 }\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}\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 return `${Math.floor(s / 86400)}d`\n}\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; font-size:18px; font-weight:700; }\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@media (max-width:480px) { .ocl-row { padding:12px 14px; } .ocl-name { font-size:14px; } }\n`\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.userId ?? persistentUid()\n const httpBase = opts.apiUrl ? opts.apiUrl.replace(/\\/+$/, '') : httpBaseFromWsUrl(opts.url)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#f5713c'\n\n // Inject CSS once\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS.replace(/#f5713c/g, accent)\n document.head.append(s)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n const head = el('div', 'ocl-head')\n head.append(el('span', 'ocl-title', i18n.title ?? 'Messages'))\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 // Track seen seqs for unread counts (persisted in localStorage)\n const seenKey = `ocl_seen_${opts.profileId}_${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 res = await fetch(\n `${httpBase}/conversations/mine?profileId=${encodeURIComponent(opts.profileId)}`,\n { headers: { authorization: `Bearer ${token}` } },\n )\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[] }\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 (e.subjectTitle ?? 'General enquiry').toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n body.append(el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.')))\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 buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = entry.subjectTitle ?? 'General enquiry'\n const initial = name[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 body.replaceChildren(el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.'))\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n return {\n refresh,\n close() {\n destroyed = true\n opts.el.replaceChildren()\n },\n }\n}\n"],"names":["timeAgo","ts","s","el","tag","cls","text","e","CSS","mountChatList","opts","token","persistentUid","httpBase","httpBaseFromWsUrl","i18n","accent","root","head","searchWrap","searchIn","body","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","res","a","b","renderRows","entries","query","filtered","isUnread","unread","read","buildRow","entry","name","initial","_a","lastSeq","unreadCount","row","av","dot","info","stateMap","right","refresh"],"mappings":"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"}
package/dist/history.d.ts CHANGED
@@ -11,4 +11,4 @@ export declare function httpBaseFromWsUrl(wsUrl: string): string;
11
11
  *
12
12
  * Returns a cleanup function — call it when the conversation is closed to
13
13
  * disconnect the observer and prevent stale updates. */
14
- export declare function restoreHistory(wsUrl: string, token: string, conversationId: ConversationId, store: ChatStore, renderer: Renderer): Promise<void>;
14
+ export declare function restoreHistory(wsUrl: string, token: string, conversationId: ConversationId, store: ChatStore, renderer: Renderer, apiBase?: string): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface UserInfo {
14
14
  export interface MountOptions {
15
15
  el: HTMLElement;
16
16
  url: string;
17
+ /** HTTP(S) base for REST calls (history, uploads, CSAT, translate). Provide
18
+ * this when the REST API is served from a different host/path than the
19
+ * WebSocket. Defaults to the WS origin with a trailing `/ws` stripped — which
20
+ * only works when WS and HTTP share an origin. */
21
+ apiUrl?: string;
17
22
  profileId: string;
18
23
  subjectId?: string;
19
24
  token?: string;