@paramms/chat-widget 1.0.31 → 1.0.33
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/chatlist.d.ts +28 -4
- package/dist/chatlist.js +63 -51
- package/dist/chatlist.js.map +1 -1
- package/dist/core.d.ts +7 -1
- package/dist/core.js +64 -63
- package/dist/core.js.map +1 -1
- package/dist/e2e.js +109 -1485
- package/dist/e2e.js.map +1 -1
- package/dist/embed.js +4 -4
- package/dist/embed.js.map +1 -1
- package/dist/hooks.d.ts +20 -2
- package/dist/hooks.js +51 -48
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -14
- package/dist/index.js.map +1 -1
- package/dist/outbox.js +1382 -0
- package/dist/outbox.js.map +1 -0
- package/dist/react.d.ts +68 -26
- package/dist/react.js +275 -235
- package/dist/react.js.map +1 -1
- package/package.json +4 -3
package/dist/hooks.d.ts
CHANGED
|
@@ -25,16 +25,34 @@ export interface RelayConversationHook {
|
|
|
25
25
|
export declare function useRelayConversation(opts: UseRelayConversationOptions): RelayConversationHook;
|
|
26
26
|
export interface ChatListRow {
|
|
27
27
|
conversationId: string;
|
|
28
|
+
/** Which chatroom this conversation belongs to. With `scope: 'tenant'` rows
|
|
29
|
+
* can come from OTHER chatrooms of the same business — open them with this
|
|
30
|
+
* profileId, not the one you queried with. */
|
|
31
|
+
profileId?: string;
|
|
32
|
+
/** 'support' (default) or 'direct' (user↔user DM). */
|
|
33
|
+
kind?: string;
|
|
34
|
+
/** For direct conversations: the other participant's user id. */
|
|
35
|
+
peerId?: string;
|
|
28
36
|
subjectId?: string;
|
|
29
37
|
title?: string;
|
|
30
38
|
lastMessage?: string;
|
|
31
39
|
updatedAt: number;
|
|
32
40
|
unread?: boolean;
|
|
33
41
|
}
|
|
34
|
-
/** The current user's conversation list
|
|
35
|
-
*
|
|
42
|
+
/** The current user's conversation list (the "inbox" screen of a chat app).
|
|
43
|
+
* Polls the REST endpoint; pass `refreshMs: 0` to fetch once.
|
|
44
|
+
*
|
|
45
|
+
* `scope` (default `'profile'`) controls which conversations are listed:
|
|
46
|
+
* • `'profile'` — only this chatroom's threads (the classic widget list).
|
|
47
|
+
* • `'tenant'` — every conversation this user has with the OWNING BUSINESS
|
|
48
|
+
* of the given profileId, across all of its chatrooms — what a real
|
|
49
|
+
* chat-app inbox shows. Rows carry `profileId` so you can open each one
|
|
50
|
+
* against the right chatroom. (The profileId is still required: it is
|
|
51
|
+
* how the server resolves which tenant "mine" means — guests never see
|
|
52
|
+
* or send tenant ids.) */
|
|
36
53
|
export declare function useRelayChatList(opts: RelayClientOptions & {
|
|
37
54
|
refreshMs?: number;
|
|
55
|
+
scope?: 'profile' | 'tenant';
|
|
38
56
|
}): {
|
|
39
57
|
conversations: ChatListRow[];
|
|
40
58
|
loading: boolean;
|
package/dist/hooks.js
CHANGED
|
@@ -1,71 +1,74 @@
|
|
|
1
|
-
import { useState as
|
|
1
|
+
import { useState as u, useEffect as h, useRef as y, useSyncExternalStore as M, useMemo as k } from "react";
|
|
2
2
|
import { RelayClient as j } from "./core.js";
|
|
3
|
-
import { r as b } from "./uid.js";
|
|
4
|
-
function
|
|
3
|
+
import { r as w, p as b } from "./uid.js";
|
|
4
|
+
function S(e) {
|
|
5
5
|
var l;
|
|
6
|
-
const
|
|
7
|
-
(!s.current || s.current.key !==
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
var
|
|
11
|
-
(
|
|
6
|
+
const a = `${e.url}|${e.token ?? ""}|${e.profileId}|${e.subjectId ?? ""}|${e.kind ?? ""}|${e.peerId ?? ""}`, s = y(null);
|
|
7
|
+
(!s.current || s.current.key !== a) && ((l = s.current) == null || l.convo.close(), s.current = { key: a, convo: new j(e).open(e) });
|
|
8
|
+
const t = s.current.convo;
|
|
9
|
+
h(() => () => {
|
|
10
|
+
var r;
|
|
11
|
+
(r = s.current) == null || r.convo.close(), s.current = null;
|
|
12
12
|
}, []);
|
|
13
13
|
const o = y(0), i = M(
|
|
14
|
-
(
|
|
15
|
-
o.current++,
|
|
14
|
+
(r) => t.onChange(() => {
|
|
15
|
+
o.current++, r();
|
|
16
16
|
}),
|
|
17
17
|
() => o.current,
|
|
18
18
|
() => o.current
|
|
19
|
-
),
|
|
19
|
+
), c = k(() => t.store.messages(), [t, i]), d = k(() => [...t.store.typing], [t, i]);
|
|
20
20
|
return {
|
|
21
|
-
messages:
|
|
22
|
-
send: (
|
|
23
|
-
typing: (
|
|
24
|
-
markRead: () =>
|
|
25
|
-
status:
|
|
26
|
-
statusMessage:
|
|
27
|
-
conversationId:
|
|
28
|
-
me:
|
|
29
|
-
peersTyping:
|
|
30
|
-
conversation:
|
|
21
|
+
messages: c,
|
|
22
|
+
send: (r) => t.send(r),
|
|
23
|
+
typing: (r) => t.typing(r),
|
|
24
|
+
markRead: () => t.markRead(),
|
|
25
|
+
status: t.status,
|
|
26
|
+
statusMessage: t.statusMessage,
|
|
27
|
+
conversationId: t.conversationId,
|
|
28
|
+
me: t.me,
|
|
29
|
+
peersTyping: d,
|
|
30
|
+
conversation: t
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
function
|
|
34
|
-
const [
|
|
35
|
-
return
|
|
36
|
-
let
|
|
37
|
-
const { httpBase:
|
|
33
|
+
function T(e) {
|
|
34
|
+
const [a, s] = u([]), [t, o] = u(!0), [i, c] = u(null), [d, l] = u(0);
|
|
35
|
+
return h(() => {
|
|
36
|
+
let r = !1;
|
|
37
|
+
const { httpBase: $ } = w(e.url, e.apiUrl), I = e.token ?? (typeof window < "u" ? b() : "");
|
|
38
38
|
async function m() {
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
41
|
-
headers:
|
|
40
|
+
const f = `profileId=${encodeURIComponent(e.profileId)}${e.scope === "tenant" ? "&scope=tenant" : ""}`, v = await fetch(`${$}/conversations/mine?${f}`, {
|
|
41
|
+
headers: I ? { authorization: `Bearer ${I}` } : {}
|
|
42
42
|
});
|
|
43
|
-
if (!
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
s(
|
|
47
|
-
conversationId:
|
|
48
|
-
...
|
|
49
|
-
...
|
|
50
|
-
...
|
|
51
|
-
|
|
52
|
-
...
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
if (!v.ok) throw new Error(`chat list failed: ${v.status}`);
|
|
44
|
+
const R = await v.json();
|
|
45
|
+
if (r) return;
|
|
46
|
+
s(R.conversations.map((n) => ({
|
|
47
|
+
conversationId: n.id,
|
|
48
|
+
...n.profileId ? { profileId: n.profileId } : {},
|
|
49
|
+
...n.kind ? { kind: n.kind } : {},
|
|
50
|
+
...n.peerId ? { peerId: n.peerId } : {},
|
|
51
|
+
...n.subjectId ? { subjectId: n.subjectId } : {},
|
|
52
|
+
...n.subjectTitle ? { title: n.subjectTitle } : {},
|
|
53
|
+
...n.lastMessage ? { lastMessage: n.lastMessage } : {},
|
|
54
|
+
updatedAt: n.updatedAt,
|
|
55
|
+
...n.unread !== void 0 ? { unread: n.unread } : {}
|
|
56
|
+
}))), c(null);
|
|
57
|
+
} catch (f) {
|
|
58
|
+
r || c(f.message);
|
|
56
59
|
} finally {
|
|
57
|
-
|
|
60
|
+
r || o(!1);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
m();
|
|
61
|
-
const
|
|
64
|
+
const p = e.refreshMs ?? 15e3, g = p > 0 ? setInterval(() => void m(), p) : null;
|
|
62
65
|
return () => {
|
|
63
|
-
|
|
66
|
+
r = !0, g && clearInterval(g);
|
|
64
67
|
};
|
|
65
|
-
}, [e.url, e.token, e.profileId, e.refreshMs,
|
|
68
|
+
}, [e.url, e.apiUrl, e.token, e.profileId, e.refreshMs, e.scope, d]), { conversations: a, loading: t, error: i, refresh: () => l((r) => r + 1) };
|
|
66
69
|
}
|
|
67
70
|
export {
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
T as useRelayChatList,
|
|
72
|
+
S as useRelayConversation
|
|
70
73
|
};
|
|
71
74
|
//# sourceMappingURL=hooks.js.map
|
package/dist/hooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","sources":["../src/hooks.ts"],"sourcesContent":["// ── @paramms/chat-widget/hooks — React bindings for the headless SDK ─────────\n// Build your own chat UI with two hooks. Same primitives as the bundled\n// widget and the agent dashboard — these are bindings, not a reimplementation.\n//\n// const { messages, send, status, typing } = useRelayConversation({\n// url, token, profileId: 'p_x', subjectId: 'listing_42',\n// })\n// const dm = useRelayConversation({ url, token, profileId: 'p_x', kind: 'direct', peerId: 'user_bob' })\n// const { conversations } = useRelayChatList({ url, token, profileId: 'p_x' })\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { RelayClient, RelayConversation, type RelayClientOptions, type OpenOptions } from './core.js'\nimport { resolveRelayUrls } from './history.js'\nimport type { RenderMessage } from './store.js'\n\nexport type UseRelayConversationOptions = RelayClientOptions & OpenOptions\n\nexport interface RelayConversationHook {\n /** Messages in seq order (optimistic sends included, pending-marked). */\n messages: RenderMessage[]\n send: (text: string) => void\n /** Call from your input's onChange for typing indicators. */\n typing: (isTyping: boolean) => void\n markRead: () => void\n /** connecting | connected | reconnecting | error */\n status: string\n statusMessage: string | undefined\n conversationId: string | undefined\n /** Our canonical user id (server-resolved) — compare with message.senderId for \"mine\". */\n me: string | undefined\n /** userIds currently typing (excluding you). */\n peersTyping: string[]\n /** The underlying conversation for anything the hook doesn't surface\n * (reactions, invoke, raw store). */\n conversation: RelayConversation\n}\n\n/** One live conversation (support thread or DM). The connection opens on\n * mount and closes on unmount; options changes reopen. */\nexport function useRelayConversation(opts: UseRelayConversationOptions): RelayConversationHook {\n // Key that captures every option that requires a NEW conversation/socket.\n const key = `${opts.url}|${opts.token ?? ''}|${opts.profileId}|${opts.subjectId ?? ''}|${opts.kind ?? ''}|${opts.peerId ?? ''}`\n const convoRef = useRef<{ key: string; convo: RelayConversation } | null>(null)\n if (!convoRef.current || convoRef.current.key !== key) {\n convoRef.current?.convo.close()\n convoRef.current = { key, convo: new RelayClient(opts).open(opts) }\n }\n const convo = convoRef.current.convo\n\n // Close on unmount (and on Strict-Mode double-invoke, reopen lazily above).\n useEffect(() => () => { convoRef.current?.convo.close(); convoRef.current = null }, [])\n\n // Subscribe React to the store via useSyncExternalStore: `version` bumps on\n // every change, and the snapshot getters below memo off it. (This is the\n // lesson from the dashboard's stale-useMemo bug baked into the API.)\n const versionRef = useRef(0)\n const version = useSyncExternalStore(\n (onStoreChange) => convo.onChange(() => { versionRef.current++; onStoreChange() }),\n () => versionRef.current,\n () => versionRef.current,\n )\n\n const messages = useMemo(() => convo.store.messages(), [convo, version])\n const peersTyping = useMemo(() => [...convo.store.typing], [convo, version])\n\n return {\n messages,\n send: (text: string) => convo.send(text),\n typing: (isTyping: boolean) => convo.typing(isTyping),\n markRead: () => convo.markRead(),\n status: convo.status,\n statusMessage: convo.statusMessage,\n conversationId: convo.conversationId as string | undefined,\n me: convo.me as string | undefined,\n peersTyping,\n conversation: convo,\n }\n}\n\nexport interface ChatListRow {\n conversationId: string\n subjectId?: string\n title?: string\n lastMessage?: string\n updatedAt: number\n unread?: boolean\n}\n\n/** The current user's conversation list for a chatroom (the \"inbox\" screen of\n * a chat app). Polls the REST endpoint; pass `refreshMs: 0` to fetch once. */\nexport function useRelayChatList(opts: RelayClientOptions & { refreshMs?: number }): {\n conversations: ChatListRow[]\n loading: boolean\n error: string | null\n refresh: () => void\n} {\n const [conversations, setConversations] = useState<ChatListRow[]>([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n const [nonce, setNonce] = useState(0)\n\n useEffect(() => {\n let cancelled = false\n const { httpBase } = resolveRelayUrls(opts.url)\n const token = opts.token ?? ''\n async function load(): Promise<void> {\n try {\n const res = await fetch(`${httpBase}/conversations/mine?profileId=${encodeURIComponent(opts.profileId)}`, {\n headers: token ? { authorization: `Bearer ${token}` } : {},\n })\n if (!res.ok) throw new Error(`chat list failed: ${res.status}`)\n const data = await res.json() as { conversations: Array<{ id: string; subjectId?: string; subjectTitle?: string; lastMessage?: string; updatedAt: number; unread?: boolean }> }\n if (cancelled) return\n setConversations(data.conversations.map(c => ({\n conversationId: c.id,\n ...(c.subjectId ? { subjectId: c.subjectId } : {}),\n ...(c.subjectTitle ? { title: c.subjectTitle } : {}),\n ...(c.lastMessage ? { lastMessage: c.lastMessage } : {}),\n updatedAt: c.updatedAt,\n ...(c.unread !== undefined ? { unread: c.unread } : {}),\n })))\n setError(null)\n } catch (e) {\n if (!cancelled) setError((e as Error).message)\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void load()\n const ms = opts.refreshMs ?? 15_000\n const timer = ms > 0 ? setInterval(() => void load(), ms) : null\n return () => { cancelled = true; if (timer) clearInterval(timer) }\n }, [opts.url, opts.token, opts.profileId, opts.refreshMs, nonce])\n\n return { conversations, loading, error, refresh: () => setNonce(n => n + 1) }\n}\n"],"names":["useRelayConversation","opts","key","convoRef","useRef","_a","RelayClient","convo","useEffect","versionRef","version","useSyncExternalStore","onStoreChange","messages","useMemo","peersTyping","text","isTyping","useRelayChatList","conversations","setConversations","useState","loading","setLoading","error","setError","nonce","setNonce","cancelled","httpBase","resolveRelayUrls","token","load","res","data","c","e","ms","timer","n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hooks.js","sources":["../src/hooks.ts"],"sourcesContent":["// ── @paramms/chat-widget/hooks — React bindings for the headless SDK ─────────\n// Build your own chat UI with two hooks. Same primitives as the bundled\n// widget and the agent dashboard — these are bindings, not a reimplementation.\n//\n// // ONE url, any scheme — 'https://api.relay.paramms.com' works everywhere.\n// const { messages, send, status, typing } = useRelayConversation({\n// url, token, profileId: 'p_x', subjectId: 'listing_42',\n// })\n// const dm = useRelayConversation({ url, token, profileId: 'p_x', kind: 'direct', peerId: 'user_bob' })\n// const { conversations } = useRelayChatList({ url, token, profileId: 'p_x' })\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { RelayClient, RelayConversation, type RelayClientOptions, type OpenOptions } from './core.js'\nimport { persistentUid } from './uid.js'\nimport { resolveRelayUrls } from './history.js'\nimport type { RenderMessage } from './store.js'\n\nexport type UseRelayConversationOptions = RelayClientOptions & OpenOptions\n\nexport interface RelayConversationHook {\n /** Messages in seq order (optimistic sends included, pending-marked). */\n messages: RenderMessage[]\n send: (text: string) => void\n /** Call from your input's onChange for typing indicators. */\n typing: (isTyping: boolean) => void\n markRead: () => void\n /** connecting | connected | reconnecting | error */\n status: string\n statusMessage: string | undefined\n conversationId: string | undefined\n /** Our canonical user id (server-resolved) — compare with message.senderId for \"mine\". */\n me: string | undefined\n /** userIds currently typing (excluding you). */\n peersTyping: string[]\n /** The underlying conversation for anything the hook doesn't surface\n * (reactions, invoke, raw store). */\n conversation: RelayConversation\n}\n\n/** One live conversation (support thread or DM). The connection opens on\n * mount and closes on unmount; options changes reopen. */\nexport function useRelayConversation(opts: UseRelayConversationOptions): RelayConversationHook {\n // Key that captures every option that requires a NEW conversation/socket.\n const key = `${opts.url}|${opts.token ?? ''}|${opts.profileId}|${opts.subjectId ?? ''}|${opts.kind ?? ''}|${opts.peerId ?? ''}`\n const convoRef = useRef<{ key: string; convo: RelayConversation } | null>(null)\n if (!convoRef.current || convoRef.current.key !== key) {\n convoRef.current?.convo.close()\n convoRef.current = { key, convo: new RelayClient(opts).open(opts) }\n }\n const convo = convoRef.current.convo\n\n // Close on unmount (and on Strict-Mode double-invoke, reopen lazily above).\n useEffect(() => () => { convoRef.current?.convo.close(); convoRef.current = null }, [])\n\n // Subscribe React to the store via useSyncExternalStore: `version` bumps on\n // every change, and the snapshot getters below memo off it. (This is the\n // lesson from the dashboard's stale-useMemo bug baked into the API.)\n const versionRef = useRef(0)\n const version = useSyncExternalStore(\n (onStoreChange) => convo.onChange(() => { versionRef.current++; onStoreChange() }),\n () => versionRef.current,\n () => versionRef.current,\n )\n\n const messages = useMemo(() => convo.store.messages(), [convo, version])\n const peersTyping = useMemo(() => [...convo.store.typing], [convo, version])\n\n return {\n messages,\n send: (text: string) => convo.send(text),\n typing: (isTyping: boolean) => convo.typing(isTyping),\n markRead: () => convo.markRead(),\n status: convo.status,\n statusMessage: convo.statusMessage,\n conversationId: convo.conversationId as string | undefined,\n me: convo.me as string | undefined,\n peersTyping,\n conversation: convo,\n }\n}\n\nexport interface ChatListRow {\n conversationId: string\n /** Which chatroom this conversation belongs to. With `scope: 'tenant'` rows\n * can come from OTHER chatrooms of the same business — open them with this\n * profileId, not the one you queried with. */\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 title?: string\n lastMessage?: string\n updatedAt: number\n unread?: boolean\n}\n\n/** The current user's conversation list (the \"inbox\" screen of a chat app).\n * Polls the REST endpoint; pass `refreshMs: 0` to fetch once.\n *\n * `scope` (default `'profile'`) controls which conversations are listed:\n * • `'profile'` — only this chatroom's threads (the classic widget list).\n * • `'tenant'` — every conversation this user has with the OWNING BUSINESS\n * of the given profileId, across all of its chatrooms — what a real\n * chat-app inbox shows. Rows carry `profileId` so you can open each one\n * against the right chatroom. (The profileId is still required: it is\n * how the server resolves which tenant \"mine\" means — guests never see\n * or send tenant ids.) */\nexport function useRelayChatList(opts: RelayClientOptions & { refreshMs?: number; scope?: 'profile' | 'tenant' }): {\n conversations: ChatListRow[]\n loading: boolean\n error: string | null\n refresh: () => void\n} {\n const [conversations, setConversations] = useState<ChatListRow[]>([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n const [nonce, setNonce] = useState(0)\n\n useEffect(() => {\n let cancelled = false\n const { httpBase } = resolveRelayUrls(opts.url, opts.apiUrl)\n // Same identity tiers as everywhere else: token > (in a browser) the\n // persistent anonymous id. Previously this defaulted to '' — anonymous\n // visitors got a 401 and the list never loaded.\n const token = opts.token ?? (typeof window !== 'undefined' ? persistentUid() : '')\n async function load(): Promise<void> {\n try {\n const qs = `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n const res = await fetch(`${httpBase}/conversations/mine?${qs}`, {\n headers: token ? { authorization: `Bearer ${token}` } : {},\n })\n if (!res.ok) throw new Error(`chat list failed: ${res.status}`)\n const data = await res.json() as { conversations: Array<{ id: string; profileId?: string; kind?: string; peerId?: string; subjectId?: string; subjectTitle?: string; lastMessage?: string; updatedAt: number; unread?: boolean }> }\n if (cancelled) return\n setConversations(data.conversations.map(c => ({\n conversationId: c.id,\n ...(c.profileId ? { profileId: c.profileId } : {}),\n ...(c.kind ? { kind: c.kind } : {}),\n ...(c.peerId ? { peerId: c.peerId } : {}),\n ...(c.subjectId ? { subjectId: c.subjectId } : {}),\n ...(c.subjectTitle ? { title: c.subjectTitle } : {}),\n ...(c.lastMessage ? { lastMessage: c.lastMessage } : {}),\n updatedAt: c.updatedAt,\n ...(c.unread !== undefined ? { unread: c.unread } : {}),\n })))\n setError(null)\n } catch (e) {\n if (!cancelled) setError((e as Error).message)\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void load()\n const ms = opts.refreshMs ?? 15_000\n const timer = ms > 0 ? setInterval(() => void load(), ms) : null\n return () => { cancelled = true; if (timer) clearInterval(timer) }\n }, [opts.url, opts.apiUrl, opts.token, opts.profileId, opts.refreshMs, opts.scope, nonce])\n\n return { conversations, loading, error, refresh: () => setNonce(n => n + 1) }\n}\n"],"names":["useRelayConversation","opts","key","convoRef","useRef","_a","RelayClient","convo","useEffect","versionRef","version","useSyncExternalStore","onStoreChange","messages","useMemo","peersTyping","text","isTyping","useRelayChatList","conversations","setConversations","useState","loading","setLoading","error","setError","nonce","setNonce","cancelled","httpBase","resolveRelayUrls","token","persistentUid","load","qs","res","data","c","e","ms","timer","n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface MountOptions {
|
|
|
21
21
|
apiUrl?: string;
|
|
22
22
|
profileId: string;
|
|
23
23
|
subjectId?: string;
|
|
24
|
+
/** Open a user↔user direct conversation with `peerId` instead of a support
|
|
25
|
+
* thread. Requires signed identity on the chatroom (both `kind: 'direct'`
|
|
26
|
+
* and `peerId` together; `subjectId` is ignored — the server derives the
|
|
27
|
+
* symmetric DM key so both sides land in the SAME conversation). */
|
|
28
|
+
kind?: 'direct';
|
|
29
|
+
peerId?: string;
|
|
24
30
|
/** IDENTITY (tiered — the host owns identity, the widget never has to persist it):
|
|
25
31
|
* 1. `token` — a signed identity token. Either a capability token, or (recommended
|
|
26
32
|
* for embedders) an ES256 JWT `{sub,iat,exp}` signed by your backend with the
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@ var ce = Object.defineProperty;
|
|
|
2
2
|
var de = (t, e, n) => e in t ? ce(t, e, { enumerable: !0, configurable: !0, writable: !0, value: n }) : t[e] = n;
|
|
3
3
|
var m = (t, e, n) => de(t, typeof e != "symbol" ? e + "" : e, n);
|
|
4
4
|
import { p as le, r as pe, a as ue } from "./uid.js";
|
|
5
|
-
import { h as
|
|
6
|
-
import { P as fe, C as he,
|
|
7
|
-
import { e as
|
|
5
|
+
import { h as Be } from "./uid.js";
|
|
6
|
+
import { P as fe, C as he, b as we } from "./outbox.js";
|
|
7
|
+
import { e as qe } from "./outbox.js";
|
|
8
|
+
import { E2ESession as be, extractX3DHInit as xe } from "./e2e.js";
|
|
8
9
|
const Q = "http://www.w3.org/2000/svg", ge = 8e3;
|
|
9
10
|
class me {
|
|
10
11
|
constructor() {
|
|
@@ -682,11 +683,11 @@ const M = /* @__PURE__ */ new WeakMap(), F = /* @__PURE__ */ new Map();
|
|
|
682
683
|
function ee(t) {
|
|
683
684
|
return `relay-launcher::${t.profileId}::${t.subjectId ?? ""}`;
|
|
684
685
|
}
|
|
685
|
-
function
|
|
686
|
+
function Le(t) {
|
|
686
687
|
var e;
|
|
687
688
|
(e = M.get(t)) == null || e.close(), M.delete(t);
|
|
688
689
|
}
|
|
689
|
-
function
|
|
690
|
+
function Ae(t) {
|
|
690
691
|
var V, Y, X, J, G;
|
|
691
692
|
M.has(t.el) && (M.get(t.el).close(), M.delete(t.el)), t.launcher && ((V = F.get(ee(t))) == null || V.close());
|
|
692
693
|
const e = le(), n = t.token ?? t.userId ?? e, s = n !== e ? e : void 0, { wsUrl: p, httpBase: d } = pe(t.url, t.apiUrl);
|
|
@@ -773,7 +774,7 @@ function Le(t) {
|
|
|
773
774
|
}
|
|
774
775
|
};
|
|
775
776
|
let g;
|
|
776
|
-
const k = new
|
|
777
|
+
const k = new be(`ocw-e2e-${t.profileId}`);
|
|
777
778
|
let $ = !1;
|
|
778
779
|
const y = [], E = [];
|
|
779
780
|
let R = !1;
|
|
@@ -887,7 +888,10 @@ function Le(t) {
|
|
|
887
888
|
} : void 0, ae = {
|
|
888
889
|
type: "open",
|
|
889
890
|
profileId: t.profileId,
|
|
890
|
-
|
|
891
|
+
// Direct conversations use the kind/peerId pair; the dm:… subject key is
|
|
892
|
+
// server-derived and owner-keyed, so passing it as subjectId from the
|
|
893
|
+
// NON-owner side would find-or-create a junk duplicate thread.
|
|
894
|
+
...t.kind === "direct" && t.peerId ? { kind: "direct", peerId: t.peerId } : t.subjectId ? { subjectId: t.subjectId } : {},
|
|
891
895
|
...s ? { linkFrom: s } : {},
|
|
892
896
|
...K ? { userInfo: K } : {},
|
|
893
897
|
...typeof location < "u" ? { pageUrl: location.href } : {},
|
|
@@ -897,7 +901,7 @@ function Le(t) {
|
|
|
897
901
|
...(J = t.subject) != null && J.title ? { subjectTitle: t.subject.title } : {},
|
|
898
902
|
...(G = t.subject) != null && G.subtitle ? { subjectMeta: t.subject.subtitle } : {}
|
|
899
903
|
};
|
|
900
|
-
g = new
|
|
904
|
+
g = new we({
|
|
901
905
|
...t.refreshToken ? { refreshToken: t.refreshToken } : {},
|
|
902
906
|
url: p,
|
|
903
907
|
token: n,
|
|
@@ -955,16 +959,16 @@ function Le(t) {
|
|
|
955
959
|
}
|
|
956
960
|
export {
|
|
957
961
|
he as ChatStore,
|
|
958
|
-
|
|
959
|
-
|
|
962
|
+
we as ConnectionManager,
|
|
963
|
+
be as E2ESession,
|
|
960
964
|
fe as PersistentOutbox,
|
|
961
965
|
Te as Renderer,
|
|
962
|
-
|
|
966
|
+
qe as asConversationId,
|
|
963
967
|
xe as extractX3DHInit,
|
|
964
|
-
|
|
965
|
-
|
|
968
|
+
Be as httpBaseFromWsUrl,
|
|
969
|
+
Ae as mount,
|
|
966
970
|
pe as resolveRelayUrls,
|
|
967
971
|
ue as restoreHistory,
|
|
968
|
-
|
|
972
|
+
Le as unmount
|
|
969
973
|
};
|
|
970
974
|
//# sourceMappingURL=index.js.map
|