@paramms/chat-widget 1.0.16 → 1.0.18
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.d.ts +3 -0
- package/dist/chatlist.js +37 -37
- package/dist/chatlist.js.map +1 -1
- package/dist/history.d.ts +21 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +771 -772
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +14 -5
- package/dist/react.js +196 -186
- package/dist/react.js.map +1 -1
- package/dist/uid.js +39 -33
- package/dist/uid.js.map +1 -1
- package/package.json +1 -1
package/dist/chatlist.d.ts
CHANGED
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { p as
|
|
1
|
+
import { p as q, r as A } from "./uid.js";
|
|
2
2
|
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,
|
|
7
|
-
const
|
|
8
|
-
return a && (
|
|
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
10
|
const _ = `
|
|
11
11
|
.ocl { --ocl-accent:#f5713c; --ocl-bg:#f3efe9; --ocl-card:#fff; --ocl-line:#ececec; --ocl-ink:#1c1b1a; --ocl-mut:#9b9690;
|
|
@@ -38,83 +38,83 @@ 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 ??
|
|
41
|
+
const a = n.userId ?? q(), { httpBase: u } = A(n.url, n.apiUrl), 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,
|
|
44
|
+
e.id = "ocl-styles", e.textContent = _.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",
|
|
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 =
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
|
53
|
+
let g = {};
|
|
54
54
|
try {
|
|
55
|
-
|
|
55
|
+
g = JSON.parse(localStorage.getItem(k) ?? "{}");
|
|
56
56
|
} catch {
|
|
57
57
|
}
|
|
58
|
-
const
|
|
58
|
+
const E = () => {
|
|
59
59
|
try {
|
|
60
|
-
localStorage.setItem(k, JSON.stringify(
|
|
60
|
+
localStorage.setItem(k, JSON.stringify(g));
|
|
61
61
|
} catch {
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
|
-
let S = [],
|
|
64
|
+
let S = [], h = !1;
|
|
65
65
|
const j = async () => {
|
|
66
66
|
const e = await fetch(
|
|
67
|
-
`${
|
|
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 (
|
|
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 (
|
|
77
|
-
|
|
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) > (
|
|
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
|
-
|
|
83
|
-
for (const t of d)
|
|
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
|
-
|
|
87
|
-
for (const t of f)
|
|
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 - (
|
|
92
|
-
e.state === "open" ? v.classList.add("open") : e.state === "awaiting_staff" && v.classList.add("waiting"),
|
|
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
|
-
const
|
|
95
|
+
const R = {
|
|
96
96
|
open: "Open",
|
|
97
97
|
awaiting_staff: "Waiting for reply…",
|
|
98
98
|
resolved: "Resolved ✓",
|
|
99
99
|
closed: "Closed"
|
|
100
100
|
};
|
|
101
|
-
m.append(o("div", "ocl-preview", e.lastMessage ??
|
|
101
|
+
m.append(o("div", "ocl-preview", e.lastMessage ?? R[e.state] ?? e.state)), t.append(m);
|
|
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 && (
|
|
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
|
-
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}).catch(() => {
|
|
111
|
-
|
|
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())),
|
|
115
|
-
refresh:
|
|
114
|
+
return p.addEventListener("input", () => C(S, p.value.trim().toLowerCase())), $(), {
|
|
115
|
+
refresh: $,
|
|
116
116
|
close() {
|
|
117
|
-
|
|
117
|
+
h = !0, n.el.replaceChildren();
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
}
|
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: '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 { resolveRelayUrls } 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 } = resolveRelayUrls(opts.url, opts.apiUrl)\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","resolveRelayUrls","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
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { ChatStore } from './store.js';
|
|
2
2
|
import type { ConversationId } from './protocol/index.js';
|
|
3
3
|
import type { Renderer } from './renderer.js';
|
|
4
|
-
/**
|
|
4
|
+
/** Resolve a single user-supplied relay URL into the concrete WebSocket URL and
|
|
5
|
+
* HTTP(S) base the client needs.
|
|
6
|
+
*
|
|
7
|
+
* Accepts any scheme — `https://`, `http://`, `wss://`, or `ws://` — and any of
|
|
8
|
+
* these shapes: bare origin (`https://api.example.com`), origin with `/ws`
|
|
9
|
+
* (`wss://api.example.com/ws`), or a sub-path (`https://api.example.com/relay`).
|
|
10
|
+
*
|
|
11
|
+
* https://api.example.com → ws wss://api.example.com/ws · http https://api.example.com
|
|
12
|
+
* wss://api.example.com/ws → ws wss://api.example.com/ws · http https://api.example.com
|
|
13
|
+
* http://localhost:3000 → ws ws://localhost:3000/ws · http http://localhost:3000
|
|
14
|
+
*
|
|
15
|
+
* `https`/`wss` map to a secure socket (`wss`); `http`/`ws` map to `ws`. A bare
|
|
16
|
+
* host with no scheme is assumed secure. Pass `apiBaseOverride` only when the
|
|
17
|
+
* REST API lives on a different origin than the socket. */
|
|
18
|
+
export declare function resolveRelayUrls(input: string, apiBaseOverride?: string): {
|
|
19
|
+
wsUrl: string;
|
|
20
|
+
httpBase: string;
|
|
21
|
+
};
|
|
22
|
+
/** Derive the HTTP(S) base origin from a ws(s):// URL.
|
|
23
|
+
* @deprecated prefer {@link resolveRelayUrls}; kept for back-compat. */
|
|
5
24
|
export declare function httpBaseFromWsUrl(wsUrl: string): string;
|
|
6
25
|
/** Initial history restore on conversation open.
|
|
7
26
|
*
|
|
@@ -11,4 +30,4 @@ export declare function httpBaseFromWsUrl(wsUrl: string): string;
|
|
|
11
30
|
*
|
|
12
31
|
* Returns a cleanup function — call it when the conversation is closed to
|
|
13
32
|
* disconnect the observer and prevent stale updates. */
|
|
14
|
-
export declare function restoreHistory(wsUrl: string, token: string, conversationId: ConversationId, store: ChatStore, renderer: Renderer): Promise<void>;
|
|
33
|
+
export declare function restoreHistory(wsUrl: string, token: string, conversationId: ConversationId, store: ChatStore, renderer: Renderer, apiBase?: string): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,12 @@ export interface UserInfo {
|
|
|
13
13
|
}
|
|
14
14
|
export interface MountOptions {
|
|
15
15
|
el: HTMLElement;
|
|
16
|
+
/** Relay URL. Any scheme works — `https://api.example.com` is fine; the widget
|
|
17
|
+
* derives the WebSocket URL (`wss://…/ws`) and REST base from it. */
|
|
16
18
|
url: string;
|
|
19
|
+
/** HTTP(S) base for REST calls — only needed when the REST API is on a
|
|
20
|
+
* DIFFERENT origin than the socket. Normally leave unset. */
|
|
21
|
+
apiUrl?: string;
|
|
17
22
|
profileId: string;
|
|
18
23
|
subjectId?: string;
|
|
19
24
|
token?: string;
|
|
@@ -54,4 +59,4 @@ export { Renderer } from './renderer.js';
|
|
|
54
59
|
export { asConversationId };
|
|
55
60
|
export { E2ESession, extractX3DHInit, type X3DHBundle } from './e2e.js';
|
|
56
61
|
export { PersistentOutbox, type OutboxItem } from './outbox.js';
|
|
57
|
-
export { restoreHistory, httpBaseFromWsUrl } from './history.js';
|
|
62
|
+
export { restoreHistory, httpBaseFromWsUrl, resolveRelayUrls } from './history.js';
|