@paramms/chat-widget 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/annotations.d.ts +11 -0
- package/dist/chatlist.d.ts +28 -4
- package/dist/chatlist.js +63 -51
- package/dist/chatlist.js.map +1 -1
- package/dist/core.d.ts +77 -0
- package/dist/core.js +146 -0
- package/dist/core.js.map +1 -0
- package/dist/e2e.js +1608 -0
- package/dist/e2e.js.map +1 -0
- package/dist/hooks.d.ts +61 -0
- package/dist/hooks.js +74 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.html +176 -0
- package/dist/react.d.ts +68 -26
- package/dist/react.js +266 -233
- package/dist/react.js.map +1 -1
- package/package.json +6 -24
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @paramms/chat-widget
|
|
|
16
16
|
import { mount } from 'https://relay.paramms.com/index.js'
|
|
17
17
|
mount({
|
|
18
18
|
el: document.getElementById('chat'),
|
|
19
|
-
url: '
|
|
19
|
+
url: 'https://api.relay.paramms.com', // ONE url, any scheme — ws + REST derived
|
|
20
20
|
profileId: 'YOUR_PROFILE_ID',
|
|
21
21
|
})
|
|
22
22
|
</script>
|
|
@@ -33,7 +33,7 @@ import { ChatWidget } from '@paramms/chat-widget/react'
|
|
|
33
33
|
export default function SupportPage({ session }) {
|
|
34
34
|
return (
|
|
35
35
|
<ChatWidget
|
|
36
|
-
url={process.env.
|
|
36
|
+
url={process.env.NEXT_PUBLIC_RELAY_URL}
|
|
37
37
|
profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}
|
|
38
38
|
userId={session?.user.id} // optional — anonymous if omitted
|
|
39
39
|
userName={session?.user.name} // optional — shown to agents
|
|
@@ -60,7 +60,7 @@ export default function ListingPage({ car, session }) {
|
|
|
60
60
|
<YourPageContent />
|
|
61
61
|
|
|
62
62
|
<MarketplaceChat
|
|
63
|
-
url={process.env.
|
|
63
|
+
url={process.env.NEXT_PUBLIC_RELAY_URL}
|
|
64
64
|
profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}
|
|
65
65
|
listingId={car.id}
|
|
66
66
|
listingTitle={car.title} // shown in chat header
|
|
@@ -77,25 +77,32 @@ export default function ListingPage({ car, session }) {
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
**
|
|
80
|
+
**Without `listingId`** → opens the buyer's single general (non-listing) thread.
|
|
81
|
+
|
|
82
|
+
**For a /messages inbox page** (WhatsApp-style thread list, tap to open, ✎ to start a new chat) use `ChatApp` — by default it lists **every conversation the user has with your business, across all your chatrooms**, and opens each against its own chatroom (`scope="tenant"`; pass `scope="profile"` for one chatroom only):
|
|
81
83
|
|
|
82
84
|
```tsx
|
|
83
85
|
// Dedicated inbox page — e.g. /messages
|
|
86
|
+
import { ChatApp } from '@paramms/chat-widget/react'
|
|
87
|
+
|
|
84
88
|
export default function MessagesPage({ session }) {
|
|
85
89
|
return (
|
|
86
90
|
<div style={{ height: '600px' }}>
|
|
87
|
-
<
|
|
88
|
-
url={process.env.
|
|
91
|
+
<ChatApp
|
|
92
|
+
url={process.env.NEXT_PUBLIC_RELAY_URL}
|
|
89
93
|
profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}
|
|
90
94
|
userId={session?.user.id}
|
|
91
95
|
userName={session?.user.name}
|
|
92
|
-
// no listingId = inbox mode, shows all threads
|
|
93
96
|
/>
|
|
94
97
|
</div>
|
|
95
98
|
)
|
|
96
99
|
}
|
|
97
100
|
```
|
|
98
101
|
|
|
102
|
+
Prefer a floating bubble that opens the same app in a panel? `<ChatAppLauncher … floating />`.
|
|
103
|
+
|
|
104
|
+
> **Multi-tenancy note:** there is no `tenantId` prop anywhere — the server resolves your tenant *from* `profileId` (the profile record carries its owning tenant, and every conversation/list query is keyed under it server-side). Guests can't spoof it, and `scope="tenant"` only widens the list to *your* chatrooms, never another business's.
|
|
105
|
+
|
|
99
106
|
### Old section below (kept for reference)
|
|
100
107
|
|
|
101
108
|
```tsx
|
|
@@ -105,7 +112,7 @@ import { ChatWidget } from '@paramms/chat-widget/react'
|
|
|
105
112
|
export default function SupportChat() {
|
|
106
113
|
return (
|
|
107
114
|
<ChatWidget
|
|
108
|
-
url="
|
|
115
|
+
url="https://api.relay.paramms.com"
|
|
109
116
|
profileId="YOUR_PROFILE_ID"
|
|
110
117
|
/>
|
|
111
118
|
)
|
|
@@ -118,7 +125,7 @@ Pass your own user's ID as the token and their details via `user`. Anonymous use
|
|
|
118
125
|
|
|
119
126
|
```tsx
|
|
120
127
|
<ChatWidget
|
|
121
|
-
url="
|
|
128
|
+
url="https://api.relay.paramms.com"
|
|
122
129
|
profileId="YOUR_PROFILE_ID"
|
|
123
130
|
token={currentUser.id} // your own stable user ID — ties history across devices
|
|
124
131
|
user={{
|
|
@@ -157,7 +164,7 @@ const signedToken = `${hdr}.${pay}.${sig}`
|
|
|
157
164
|
|
|
158
165
|
```tsx
|
|
159
166
|
<ChatWidget
|
|
160
|
-
url="
|
|
167
|
+
url="https://api.relay.paramms.com"
|
|
161
168
|
profileId="YOUR_PROFILE_ID"
|
|
162
169
|
subjectId={`car_${listing.id}`} // one conversation per item
|
|
163
170
|
showChatList={true} // guest can switch between their threads
|
|
@@ -168,7 +175,7 @@ const signedToken = `${hdr}.${pay}.${sig}`
|
|
|
168
175
|
|
|
169
176
|
```tsx
|
|
170
177
|
<ChatWidget
|
|
171
|
-
url="
|
|
178
|
+
url="https://api.relay.paramms.com"
|
|
172
179
|
profileId="YOUR_PROFILE_ID"
|
|
173
180
|
launcher={true}
|
|
174
181
|
position="bottom-right"
|
|
@@ -180,7 +187,7 @@ const signedToken = `${hdr}.${pay}.${sig}`
|
|
|
180
187
|
|
|
181
188
|
```tsx
|
|
182
189
|
<ChatWidget
|
|
183
|
-
url="
|
|
190
|
+
url="https://api.relay.paramms.com"
|
|
184
191
|
profileId="YOUR_PROFILE_ID"
|
|
185
192
|
i18n={{
|
|
186
193
|
placeholder: 'Écrivez un message…',
|
|
@@ -198,7 +205,7 @@ RTL is detected automatically for Arabic, Hebrew, Persian and Urdu browsers.
|
|
|
198
205
|
| Option | Type | Default | Description |
|
|
199
206
|
|---|---|---|---|
|
|
200
207
|
| `el` | `HTMLElement` | required | Mount target |
|
|
201
|
-
| `url` | `string` | required |
|
|
208
|
+
| `url` | `string` | required | Relay URL — ONE url, any scheme (`https://api.relay.paramms.com`); the WebSocket URL and REST base are derived |
|
|
202
209
|
| `profileId` | `string` | required | Domain profile ID |
|
|
203
210
|
| `token` | `string` | auto-generated | Guest identity token — pass your user's stable ID to tie history across devices |
|
|
204
211
|
| `user` | `UserInfo` | — | Name, email, avatar, custom metadata — shown to agents |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerFrame } from './protocol/index.js';
|
|
2
|
+
export declare class AnnotationOverlay {
|
|
3
|
+
private svg;
|
|
4
|
+
private readonly timers;
|
|
5
|
+
/** Feed every server frame; the overlay reacts to annotation frames only. */
|
|
6
|
+
apply(frame: ServerFrame): void;
|
|
7
|
+
private ensureSvg;
|
|
8
|
+
private draw;
|
|
9
|
+
clear(): void;
|
|
10
|
+
destroy(): void;
|
|
11
|
+
}
|
package/dist/chatlist.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* import { mountChatList } from '@paramms/chat-widget/chatlist'
|
|
11
11
|
* const handle = mountChatList({
|
|
12
12
|
* el: document.getElementById('chat-list'),
|
|
13
|
-
* url: '
|
|
13
|
+
* url: 'https://api.relay.paramms.com', // ONE url, any scheme
|
|
14
14
|
* profileId: 'p_usedcars',
|
|
15
15
|
* userId: currentUser.id, // optional — uses localStorage UID if omitted
|
|
16
16
|
* onSelect: (entry) => {
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export interface ChatListEntry {
|
|
24
24
|
id: string;
|
|
25
|
+
/** Chatroom this conversation belongs to. With `scope: 'tenant'` this can
|
|
26
|
+
* differ from the profileId the list was mounted with — open the chat
|
|
27
|
+
* against THIS profileId. */
|
|
28
|
+
profileId?: string;
|
|
29
|
+
/** 'support' (default) or 'direct' (user↔user DM). */
|
|
30
|
+
kind?: string;
|
|
31
|
+
/** For direct conversations: the other participant's user id. */
|
|
32
|
+
peerId?: string;
|
|
25
33
|
subjectId?: string;
|
|
26
34
|
subjectTitle?: string;
|
|
27
35
|
/** One-line detail — e.g. "45,000 km · Auto" */
|
|
@@ -36,17 +44,32 @@ export interface ChatListEntry {
|
|
|
36
44
|
export interface ChatListOptions {
|
|
37
45
|
/** Mount target element */
|
|
38
46
|
el: HTMLElement;
|
|
39
|
-
/** Relay
|
|
47
|
+
/** Relay URL — ONE url, any scheme (https recommended). The WebSocket URL
|
|
48
|
+
* and REST base are derived automatically. */
|
|
40
49
|
url: string;
|
|
41
|
-
/** HTTP(S) base for
|
|
42
|
-
*
|
|
50
|
+
/** HTTP(S) base for REST — only when REST is on a different origin.
|
|
51
|
+
* @deprecated pass a single `url`; kept for back-compat. */
|
|
43
52
|
apiUrl?: string;
|
|
44
53
|
/** Profile ID to scope conversations to */
|
|
45
54
|
profileId: string;
|
|
55
|
+
/** A signed identity token (ES256 JWT) — the production identity tier for
|
|
56
|
+
* chatrooms with signed identity enabled. Wins over `userId`. */
|
|
57
|
+
token?: string;
|
|
46
58
|
/** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */
|
|
47
59
|
userId?: string;
|
|
60
|
+
/** Which conversations to list (default 'profile'):
|
|
61
|
+
* 'profile' — only this chatroom's threads.
|
|
62
|
+
* 'tenant' — every conversation this user has with the chatroom's owning
|
|
63
|
+
* business, across ALL of its chatrooms (a real chat-app inbox). Rows
|
|
64
|
+
* carry `profileId` so each opens against the right chatroom. */
|
|
65
|
+
scope?: 'profile' | 'tenant';
|
|
48
66
|
/** Called when the user taps a conversation row */
|
|
49
67
|
onSelect: (entry: ChatListEntry) => void;
|
|
68
|
+
/** When provided, the list shows a ✎ compose button in the header (and a
|
|
69
|
+
* "Start a conversation" button in the empty state) that calls this —
|
|
70
|
+
* wire it to open a fresh/general thread. Without it a user with no
|
|
71
|
+
* conversations yet has nothing to tap. */
|
|
72
|
+
onNewChat?: () => void;
|
|
50
73
|
/** Brand colour hex — default '#4F63F5' */
|
|
51
74
|
accent?: string;
|
|
52
75
|
/** i18n overrides */
|
|
@@ -57,6 +80,7 @@ export interface ChatListOptions {
|
|
|
57
80
|
unread?: string;
|
|
58
81
|
all?: string;
|
|
59
82
|
error?: string;
|
|
83
|
+
newChat?: string;
|
|
60
84
|
};
|
|
61
85
|
}
|
|
62
86
|
export interface ChatListHandle {
|
package/dist/chatlist.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { p as
|
|
2
|
-
function
|
|
3
|
-
const
|
|
4
|
-
return
|
|
1
|
+
import { p as A, r as _ } from "./uid.js";
|
|
2
|
+
function B(n) {
|
|
3
|
+
const r = Math.floor((Date.now() - n) / 1e3);
|
|
4
|
+
return r < 60 ? "just now" : r < 3600 ? `${Math.floor(r / 60)}m` : r < 86400 ? `${Math.floor(r / 3600)}h` : `${Math.floor(r / 86400)}d`;
|
|
5
5
|
}
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
return
|
|
6
|
+
function t(n, r, u) {
|
|
7
|
+
const a = document.createElement(n);
|
|
8
|
+
return r && (a.className = r), u !== void 0 && (a.textContent = u), a;
|
|
9
9
|
}
|
|
10
|
-
const
|
|
10
|
+
const O = `
|
|
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; }
|
|
@@ -35,90 +35,102 @@ const _ = `
|
|
|
35
35
|
.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }
|
|
36
36
|
.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; }
|
|
37
37
|
.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }
|
|
38
|
+
.ocl-compose { border:none; background:var(--ocl-bg); color:var(--ocl-ink); width:32px; height:32px; border-radius:50%; font-size:15px; cursor:pointer; }
|
|
39
|
+
.ocl-compose:hover { background:var(--ocl-line); }
|
|
40
|
+
.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:#fff; border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
|
|
38
41
|
@media (max-width:480px) { .ocl-row { padding:12px 14px; } .ocl-name { font-size:14px; } }
|
|
39
42
|
`;
|
|
40
|
-
function
|
|
41
|
-
const
|
|
43
|
+
function F(n) {
|
|
44
|
+
const r = n.token ?? n.userId ?? A(), { httpBase: u } = _(n.url, n.apiUrl), a = n.i18n ?? {}, E = n.accent ?? "#4F63F5";
|
|
42
45
|
if (!document.getElementById("ocl-styles")) {
|
|
43
46
|
const e = document.createElement("style");
|
|
44
|
-
e.id = "ocl-styles", e.textContent =
|
|
47
|
+
e.id = "ocl-styles", e.textContent = O.replace(/#f5713c/g, E), document.head.append(e);
|
|
45
48
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const k = t("div", "ocl"), m = t("div", "ocl-head");
|
|
50
|
+
if (m.append(t("span", "ocl-title", a.title ?? "Messages")), n.onNewChat) {
|
|
51
|
+
const e = t("button", "ocl-compose", "✎");
|
|
52
|
+
e.title = a.newChat ?? "New conversation", e.addEventListener("click", () => n.onNewChat()), m.append(e);
|
|
53
|
+
}
|
|
54
|
+
const C = t("div", "ocl-search-wrap"), p = t("input", "ocl-search");
|
|
55
|
+
p.placeholder = a.search ?? "🔍 Search", p.type = "search", C.append(p);
|
|
56
|
+
const c = t("div", "ocl-body");
|
|
57
|
+
c.append(t("div", "ocl-spinner", "Loading…")), k.append(m, C, c), n.el.replaceChildren(k);
|
|
58
|
+
const y = `ocl_seen_${n.profileId}_${(n.userId ?? r).slice(-8)}`;
|
|
53
59
|
let g = {};
|
|
54
60
|
try {
|
|
55
|
-
g = JSON.parse(localStorage.getItem(
|
|
61
|
+
g = JSON.parse(localStorage.getItem(y) ?? "{}");
|
|
56
62
|
} catch {
|
|
57
63
|
}
|
|
58
|
-
const
|
|
64
|
+
const U = () => {
|
|
59
65
|
try {
|
|
60
|
-
localStorage.setItem(
|
|
66
|
+
localStorage.setItem(y, JSON.stringify(g));
|
|
61
67
|
} catch {
|
|
62
68
|
}
|
|
63
69
|
};
|
|
64
70
|
let S = [], h = !1;
|
|
65
|
-
const
|
|
71
|
+
const R = async () => {
|
|
66
72
|
const e = await fetch(
|
|
67
|
-
`${u}/conversations/mine?profileId=${encodeURIComponent(n.profileId)}`,
|
|
68
|
-
{ headers: { authorization: `Bearer ${
|
|
73
|
+
`${u}/conversations/mine?profileId=${encodeURIComponent(n.profileId)}${n.scope === "tenant" ? "&scope=tenant" : ""}`,
|
|
74
|
+
{ headers: { authorization: `Bearer ${r}` } }
|
|
69
75
|
);
|
|
70
76
|
return e.ok ? ((await e.json()).conversations ?? []).sort((l, s) => s.updatedAt - l.updatedAt) : [];
|
|
71
|
-
},
|
|
77
|
+
}, z = (e, i) => {
|
|
72
78
|
if (h) return;
|
|
73
79
|
const l = i ? e.filter(
|
|
74
|
-
(
|
|
80
|
+
(o) => L(o).toLowerCase().includes(i) || (o.lastMessage ?? "").toLowerCase().includes(i)
|
|
75
81
|
) : e;
|
|
76
82
|
if (c.replaceChildren(), !l.length) {
|
|
77
|
-
|
|
83
|
+
const o = t("div", "ocl-empty", i ? "No results." : a.empty ?? "No conversations yet.");
|
|
84
|
+
if (!i && n.onNewChat) {
|
|
85
|
+
o.append(t("br"));
|
|
86
|
+
const x = t("button", "ocl-start", a.newChat ?? "Start a conversation");
|
|
87
|
+
x.addEventListener("click", () => n.onNewChat()), o.append(x);
|
|
88
|
+
}
|
|
89
|
+
c.append(o);
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
80
|
-
const s = (
|
|
92
|
+
const s = (o) => (o.lastSeq ?? 0) > (g[o.id] ?? 0), d = l.filter(s), f = l.filter((o) => !s(o));
|
|
81
93
|
if (d.length) {
|
|
82
|
-
c.append(
|
|
83
|
-
for (const
|
|
94
|
+
c.append(t("div", "ocl-section", `${a.unread ?? "Unread"} (${d.length})`));
|
|
95
|
+
for (const o of d) c.append($(o, s(o)));
|
|
84
96
|
}
|
|
85
97
|
if (f.length) {
|
|
86
|
-
c.append(
|
|
87
|
-
for (const
|
|
98
|
+
c.append(t("div", "ocl-section", d.length ? a.all ?? "All conversations" : ""));
|
|
99
|
+
for (const o of f) c.append($(o, !1));
|
|
88
100
|
}
|
|
89
|
-
},
|
|
90
|
-
var
|
|
91
|
-
const l = e
|
|
92
|
-
e.state === "open" ?
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const
|
|
101
|
+
}, L = (e) => e.subjectTitle ?? (e.kind === "direct" ? e.peerId ?? "Direct message" : "General enquiry"), $ = (e, i) => {
|
|
102
|
+
var N;
|
|
103
|
+
const l = L(e), s = ((N = l[0]) == null ? void 0 : N.toUpperCase()) ?? "?", d = e.lastSeq ?? 0, f = i ? Math.max(1, d - (g[e.id] ?? 0)) : 0, o = t("button", `ocl-row${i ? " unread" : ""}`), x = t("div", "ocl-av", s), w = t("div", "ocl-dot");
|
|
104
|
+
e.state === "open" ? w.classList.add("open") : e.state === "awaiting_staff" && w.classList.add("waiting"), x.append(w), o.append(x);
|
|
105
|
+
const b = t("div", "ocl-info");
|
|
106
|
+
b.append(t("div", "ocl-name", l));
|
|
107
|
+
const j = {
|
|
96
108
|
open: "Open",
|
|
97
109
|
awaiting_staff: "Waiting for reply…",
|
|
98
110
|
resolved: "Resolved ✓",
|
|
99
111
|
closed: "Closed"
|
|
100
112
|
};
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
return
|
|
113
|
+
b.append(t("div", "ocl-preview", e.lastMessage ?? j[e.state] ?? e.state)), o.append(b);
|
|
114
|
+
const v = t("div", "ocl-right");
|
|
115
|
+
return v.append(t("div", "ocl-time", B(e.updatedAt))), f > 0 && v.append(t("div", "ocl-badge", String(f > 99 ? "99+" : f))), o.append(v), o.addEventListener("click", () => {
|
|
104
116
|
var M;
|
|
105
|
-
d > 0 && (g[e.id] = d,
|
|
106
|
-
}),
|
|
107
|
-
},
|
|
108
|
-
h ||
|
|
109
|
-
h || (S = e,
|
|
117
|
+
d > 0 && (g[e.id] = d, U()), o.classList.remove("unread"), (M = v.querySelector(".ocl-badge")) == null || M.remove(), n.onSelect(e);
|
|
118
|
+
}), o;
|
|
119
|
+
}, I = () => {
|
|
120
|
+
h || R().then((e) => {
|
|
121
|
+
h || (S = e, z(e, p.value.trim().toLowerCase()));
|
|
110
122
|
}).catch((e) => {
|
|
111
|
-
h || (console.error(`[chat-widget] failed to load conversations from ${u}/conversations/mine — check the apiUrl/CORS config.`, e), c.replaceChildren(
|
|
123
|
+
h || (console.error(`[chat-widget] failed to load conversations from ${u}/conversations/mine — check the apiUrl/CORS config.`, e), c.replaceChildren(t("div", "ocl-empty", a.error ?? "Could not load conversations.")));
|
|
112
124
|
});
|
|
113
125
|
};
|
|
114
|
-
return p.addEventListener("input", () =>
|
|
115
|
-
refresh:
|
|
126
|
+
return p.addEventListener("input", () => z(S, p.value.trim().toLowerCase())), I(), {
|
|
127
|
+
refresh: I,
|
|
116
128
|
close() {
|
|
117
129
|
h = !0, n.el.replaceChildren();
|
|
118
130
|
}
|
|
119
131
|
};
|
|
120
132
|
}
|
|
121
133
|
export {
|
|
122
|
-
|
|
134
|
+
F as mountChatList
|
|
123
135
|
};
|
|
124
136
|
//# sourceMappingURL=chatlist.js.map
|
package/dist/chatlist.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatlist.js","sources":["../src/chatlist.ts"],"sourcesContent":["/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: '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 '#4F63F5' */\n accent?: string\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n }\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 ?? '#4F63F5'\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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"}
|
|
1
|
+
{"version":3,"file":"chatlist.js","sources":["../src/chatlist.ts"],"sourcesContent":["/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: 'https://api.relay.paramms.com', // ONE url, any scheme\n * profileId: 'p_usedcars',\n * userId: currentUser.id, // optional — uses localStorage UID if omitted\n * onSelect: (entry) => {\n * window.location.href = `/listings/${entry.subjectId}#chat`\n * },\n * })\n * handle.refresh() // manually re-fetch the list\n * handle.close() // unmount and clean up\n */\n\nimport { resolveRelayUrls } from './history.js'\nimport { persistentUid } from './uid.js'\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 /** 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 /** Brand colour hex — default '#4F63F5' */\n accent?: string\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n 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}\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.ocl-compose { border:none; background:var(--ocl-bg); color:var(--ocl-ink); width:32px; height:32px; border-radius:50%; font-size:15px; cursor:pointer; }\n.ocl-compose:hover { background:var(--ocl-line); }\n.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:#fff; border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n@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.token ?? opts.userId ?? persistentUid()\n const { httpBase } = resolveRelayUrls(opts.url, opts.apiUrl)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#4F63F5'\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 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 // 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 const seenKey = `ocl_seen_${opts.profileId}_${(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 res = await fetch(\n `${httpBase}/conversations/mine?profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`,\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 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 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","compose","searchWrap","searchIn","body","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","res","a","b","renderRows","entries","query","filtered","rowName","empty","start","isUnread","unread","read","buildRow","entry","name","initial","_a","lastSeq","unreadCount","row","av","dot","info","stateMap","right","refresh"],"mappings":"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"}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export { ConnectionManager, type SocketLike } from './connection.js';
|
|
2
|
+
export { ChatStore } from './store.js';
|
|
3
|
+
export { PersistentOutbox } from './outbox.js';
|
|
4
|
+
export { E2ESession } from './e2e.js';
|
|
5
|
+
export { restoreHistory, resolveRelayUrls, httpBaseFromWsUrl } from './history.js';
|
|
6
|
+
export { mountChatList, type ChatListEntry, type ChatListHandle, type ChatListOptions } from './chatlist.js';
|
|
7
|
+
export { persistentUid } from './uid.js';
|
|
8
|
+
export * from './protocol/index.js';
|
|
9
|
+
import { ChatStore } from './store.js';
|
|
10
|
+
import type { ConversationId, UserId } from './protocol/index.js';
|
|
11
|
+
export interface RelayClientOptions {
|
|
12
|
+
/** Relay URL — ONE url, any scheme. `https://api.relay.paramms.com` is the
|
|
13
|
+
* recommended form; the WebSocket URL (`wss://…/ws`) and REST base are
|
|
14
|
+
* derived from it automatically. `wss://`/`ws://`/`http://` also accepted. */
|
|
15
|
+
url: string;
|
|
16
|
+
/** HTTP(S) base for REST calls — only when the REST API lives on a
|
|
17
|
+
* DIFFERENT origin than the socket. Normally omit.
|
|
18
|
+
* @deprecated pass a single `url`; kept for back-compat. */
|
|
19
|
+
apiUrl?: string;
|
|
20
|
+
/** Identity: a signed JWT (secure), a stable userId (host-vouched), or omit
|
|
21
|
+
* for an anonymous per-browser guest (browser environments only). */
|
|
22
|
+
token?: string;
|
|
23
|
+
/** Chatroom id (from the dashboard). Required to open conversations. */
|
|
24
|
+
profileId: string;
|
|
25
|
+
}
|
|
26
|
+
export interface OpenOptions {
|
|
27
|
+
/** Support thread scoped to a subject (listing/order/…): one thread per
|
|
28
|
+
* (user, subject). Omit for the profile's single support thread. */
|
|
29
|
+
subjectId?: string;
|
|
30
|
+
subjectTitle?: string;
|
|
31
|
+
/** User↔user conversation (requires the chatroom to have signed identity
|
|
32
|
+
* and `token` to be a valid signed JWT). */
|
|
33
|
+
kind?: 'direct';
|
|
34
|
+
peerId?: string;
|
|
35
|
+
/** Display info persisted for agents (support threads only). */
|
|
36
|
+
user?: {
|
|
37
|
+
name?: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
avatar?: string;
|
|
40
|
+
meta?: Record<string, string>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** One conversation = one connection + one store. Deliberately thin: the
|
|
44
|
+
* store is the source of truth, `onChange` is the render signal, everything
|
|
45
|
+
* else is the same primitives the first-party UIs use. */
|
|
46
|
+
export declare class RelayConversation {
|
|
47
|
+
readonly store: ChatStore;
|
|
48
|
+
private readonly conn;
|
|
49
|
+
private readonly listeners;
|
|
50
|
+
private msgSeq;
|
|
51
|
+
private _status;
|
|
52
|
+
private _statusMessage;
|
|
53
|
+
constructor(opts: RelayClientOptions & OpenOptions & {
|
|
54
|
+
me: UserId;
|
|
55
|
+
});
|
|
56
|
+
/** Subscribe to any change (message, typing, status). Returns unsubscribe. */
|
|
57
|
+
onChange(fn: () => void): () => void;
|
|
58
|
+
private emit;
|
|
59
|
+
private _me;
|
|
60
|
+
/** Our canonical user id as resolved by the server (JWT sub / userId / anon id). */
|
|
61
|
+
get me(): UserId | undefined;
|
|
62
|
+
get conversationId(): ConversationId | undefined;
|
|
63
|
+
get status(): string;
|
|
64
|
+
get statusMessage(): string | undefined;
|
|
65
|
+
send(text: string): void;
|
|
66
|
+
typing(isTyping: boolean, preview?: string): void;
|
|
67
|
+
markRead(): void;
|
|
68
|
+
close(): void;
|
|
69
|
+
}
|
|
70
|
+
export declare class RelayClient {
|
|
71
|
+
private readonly opts;
|
|
72
|
+
constructor(opts: RelayClientOptions);
|
|
73
|
+
/** The identity this client will act as: the token's subject (resolved
|
|
74
|
+
* server-side), the raw userId, or a persistent anonymous browser id. */
|
|
75
|
+
me(): UserId;
|
|
76
|
+
open(open?: OpenOptions): RelayConversation;
|
|
77
|
+
}
|