@paramms/chat-widget 1.0.32 → 1.0.34

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/hooks.d.ts CHANGED
@@ -50,7 +50,14 @@ export interface ChatListRow {
50
50
  * against the right chatroom. (The profileId is still required: it is
51
51
  * how the server resolves which tenant "mine" means — guests never see
52
52
  * or send tenant ids.) */
53
- export declare function useRelayChatList(opts: RelayClientOptions & {
53
+ export declare function useRelayChatList(opts: Omit<RelayClientOptions, 'profileId'> & {
54
+ /** A chatroom id — or omit and pass `tenantId` for tenant-level listing. */
55
+ profileId?: string;
56
+ /** Tenant-level identification: list EVERY conversation this user has with
57
+ * the business across ALL of its chatrooms, no profileId needed (e.g. a
58
+ * platform with a marketplace chatroom AND a general-support chatroom).
59
+ * Provide `profileId` OR `tenantId`. */
60
+ tenantId?: string;
54
61
  refreshMs?: number;
55
62
  scope?: 'profile' | 'tenant';
56
63
  }): {
@@ -58,4 +65,7 @@ export declare function useRelayChatList(opts: RelayClientOptions & {
58
65
  loading: boolean;
59
66
  error: string | null;
60
67
  refresh: () => void;
68
+ /** Where a "new conversation" should open: the configured profileId, else
69
+ * the server-reported tenant default (oldest chatroom). */
70
+ defaultProfileId: string | undefined;
61
71
  };
package/dist/hooks.js CHANGED
@@ -1,49 +1,53 @@
1
- import { useState as u, useEffect as h, useRef as y, useSyncExternalStore as M, useMemo as k } from "react";
2
- import { RelayClient as j } from "./core.js";
3
- import { r as w, p as b } from "./uid.js";
4
- function S(e) {
5
- var l;
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;
1
+ import { useState as d, useEffect as R, useRef as $, useSyncExternalStore as w, useMemo as h } from "react";
2
+ import { RelayClient as C } from "./core.js";
3
+ import { r as b, p as U } from "./uid.js";
4
+ function T(e) {
5
+ var c;
6
+ const l = `${e.url}|${e.token ?? ""}|${e.profileId}|${e.subjectId ?? ""}|${e.kind ?? ""}|${e.peerId ?? ""}`, o = $(null);
7
+ (!o.current || o.current.key !== l) && ((c = o.current) == null || c.convo.close(), o.current = { key: l, convo: new C(e).open(e) });
8
+ const r = o.current.convo;
9
+ R(() => () => {
10
+ var t;
11
+ (t = o.current) == null || t.convo.close(), o.current = null;
12
12
  }, []);
13
- const o = y(0), i = M(
14
- (r) => t.onChange(() => {
15
- o.current++, r();
13
+ const s = $(0), u = w(
14
+ (t) => r.onChange(() => {
15
+ s.current++, t();
16
16
  }),
17
- () => o.current,
18
- () => o.current
19
- ), c = k(() => t.store.messages(), [t, i]), d = k(() => [...t.store.typing], [t, i]);
17
+ () => s.current,
18
+ () => s.current
19
+ ), i = h(() => r.store.messages(), [r, u]), f = h(() => [...r.store.typing], [r, u]);
20
20
  return {
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
21
+ messages: i,
22
+ send: (t) => r.send(t),
23
+ typing: (t) => r.typing(t),
24
+ markRead: () => r.markRead(),
25
+ status: r.status,
26
+ statusMessage: r.statusMessage,
27
+ conversationId: r.conversationId,
28
+ me: r.me,
29
+ peersTyping: f,
30
+ conversation: r
31
31
  };
32
32
  }
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
- async function m() {
33
+ function x(e) {
34
+ const [l, o] = d([]), [r, s] = d(!0), [u, i] = d(null), [f, c] = d(0), [t, M] = d(void 0);
35
+ return R(() => {
36
+ if (!e.profileId && !e.tenantId) {
37
+ i("provide profileId or tenantId"), s(!1);
38
+ return;
39
+ }
40
+ let a = !1;
41
+ const { httpBase: j } = b(e.url, e.apiUrl), p = e.token ?? (typeof window < "u" ? U() : "");
42
+ async function g() {
39
43
  try {
40
- const f = `profileId=${encodeURIComponent(e.profileId)}${e.scope === "tenant" ? "&scope=tenant" : ""}`, v = await fetch(`${$}/conversations/mine?${f}`, {
41
- headers: I ? { authorization: `Bearer ${I}` } : {}
44
+ const I = e.profileId ? `profileId=${encodeURIComponent(e.profileId)}${e.scope === "tenant" ? "&scope=tenant" : ""}` : `tenantId=${encodeURIComponent(e.tenantId)}`, v = await fetch(`${j}/conversations/mine?${I}`, {
45
+ headers: p ? { authorization: `Bearer ${p}` } : {}
42
46
  });
43
47
  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) => ({
48
+ const m = await v.json();
49
+ if (m.defaultProfileId && M(m.defaultProfileId), a) return;
50
+ o(m.conversations.map((n) => ({
47
51
  conversationId: n.id,
48
52
  ...n.profileId ? { profileId: n.profileId } : {},
49
53
  ...n.kind ? { kind: n.kind } : {},
@@ -53,22 +57,28 @@ function T(e) {
53
57
  ...n.lastMessage ? { lastMessage: n.lastMessage } : {},
54
58
  updatedAt: n.updatedAt,
55
59
  ...n.unread !== void 0 ? { unread: n.unread } : {}
56
- }))), c(null);
57
- } catch (f) {
58
- r || c(f.message);
60
+ }))), i(null);
61
+ } catch (I) {
62
+ a || i(I.message);
59
63
  } finally {
60
- r || o(!1);
64
+ a || s(!1);
61
65
  }
62
66
  }
63
- m();
64
- const p = e.refreshMs ?? 15e3, g = p > 0 ? setInterval(() => void m(), p) : null;
67
+ g();
68
+ const y = e.refreshMs ?? 15e3, k = y > 0 ? setInterval(() => void g(), y) : null;
65
69
  return () => {
66
- r = !0, g && clearInterval(g);
70
+ a = !0, k && clearInterval(k);
67
71
  };
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) };
72
+ }, [e.url, e.apiUrl, e.token, e.profileId, e.tenantId, e.refreshMs, e.scope, f]), {
73
+ conversations: l,
74
+ loading: r,
75
+ error: u,
76
+ refresh: () => c((a) => a + 1),
77
+ defaultProfileId: e.profileId ?? t
78
+ };
69
79
  }
70
80
  export {
71
- T as useRelayChatList,
72
- S as useRelayConversation
81
+ x as useRelayChatList,
82
+ T as useRelayConversation
73
83
  };
74
84
  //# 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// // 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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"}
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: Omit<RelayClientOptions, 'profileId'> & {\n /** A chatroom id — or omit and pass `tenantId` for tenant-level listing. */\n profileId?: string\n /** Tenant-level identification: list EVERY conversation this user has with\n * the business across ALL of its chatrooms, no profileId needed (e.g. a\n * platform with a marketplace chatroom AND a general-support chatroom).\n * Provide `profileId` OR `tenantId`. */\n tenantId?: string\n refreshMs?: number\n scope?: 'profile' | 'tenant'\n}): {\n conversations: ChatListRow[]\n loading: boolean\n error: string | null\n refresh: () => void\n /** Where a \"new conversation\" should open: the configured profileId, else\n * the server-reported tenant default (oldest chatroom). */\n defaultProfileId: string | undefined\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 const [serverDefault, setServerDefault] = useState<string | undefined>(undefined)\n\n useEffect(() => {\n if (!opts.profileId && !opts.tenantId) { setError('provide profileId or tenantId'); setLoading(false); return }\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 = opts.profileId\n ? `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n : `tenantId=${encodeURIComponent(opts.tenantId!)}`\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 }>; defaultProfileId?: string }\n if (data.defaultProfileId) setServerDefault(data.defaultProfileId)\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.tenantId, opts.refreshMs, opts.scope, nonce])\n\n return {\n conversations, loading, error,\n refresh: () => setNonce(n => n + 1),\n defaultProfileId: opts.profileId ?? serverDefault,\n }\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","serverDefault","setServerDefault","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