@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/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 for a chatroom (the "inbox" screen of
35
- * a chat app). Polls the REST endpoint; pass `refreshMs: 0` to fetch once. */
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 d, useEffect as k, useRef as y, useSyncExternalStore as M, useMemo as h } from "react";
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 E(e) {
3
+ import { r as w, p as b } from "./uid.js";
4
+ function S(e) {
5
5
  var l;
6
- const c = `${e.url}|${e.token ?? ""}|${e.profileId}|${e.subjectId ?? ""}|${e.kind ?? ""}|${e.peerId ?? ""}`, s = y(null);
7
- (!s.current || s.current.key !== c) && ((l = s.current) == null || l.convo.close(), s.current = { key: c, convo: new j(e).open(e) });
8
- const n = s.current.convo;
9
- k(() => () => {
10
- var t;
11
- (t = s.current) == null || t.convo.close(), s.current = null;
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
- (t) => n.onChange(() => {
15
- o.current++, t();
14
+ (r) => t.onChange(() => {
15
+ o.current++, r();
16
16
  }),
17
17
  () => o.current,
18
18
  () => o.current
19
- ), u = h(() => n.store.messages(), [n, i]), f = h(() => [...n.store.typing], [n, i]);
19
+ ), c = k(() => t.store.messages(), [t, i]), d = k(() => [...t.store.typing], [t, i]);
20
20
  return {
21
- messages: u,
22
- send: (t) => n.send(t),
23
- typing: (t) => n.typing(t),
24
- markRead: () => n.markRead(),
25
- status: n.status,
26
- statusMessage: n.statusMessage,
27
- conversationId: n.conversationId,
28
- me: n.me,
29
- peersTyping: f,
30
- conversation: n
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 S(e) {
34
- const [c, s] = d([]), [n, o] = d(!0), [i, u] = d(null), [f, l] = d(0);
35
- return k(() => {
36
- let t = !1;
37
- const { httpBase: R } = b(e.url), v = e.token ?? "";
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 a = await fetch(`${R}/conversations/mine?profileId=${encodeURIComponent(e.profileId)}`, {
41
- headers: v ? { authorization: `Bearer ${v}` } : {}
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 (!a.ok) throw new Error(`chat list failed: ${a.status}`);
44
- const $ = await a.json();
45
- if (t) return;
46
- s($.conversations.map((r) => ({
47
- conversationId: r.id,
48
- ...r.subjectId ? { subjectId: r.subjectId } : {},
49
- ...r.subjectTitle ? { title: r.subjectTitle } : {},
50
- ...r.lastMessage ? { lastMessage: r.lastMessage } : {},
51
- updatedAt: r.updatedAt,
52
- ...r.unread !== void 0 ? { unread: r.unread } : {}
53
- }))), u(null);
54
- } catch (a) {
55
- t || u(a.message);
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
- t || o(!1);
60
+ r || o(!1);
58
61
  }
59
62
  }
60
63
  m();
61
- const g = e.refreshMs ?? 15e3, I = g > 0 ? setInterval(() => void m(), g) : null;
64
+ const p = e.refreshMs ?? 15e3, g = p > 0 ? setInterval(() => void m(), p) : null;
62
65
  return () => {
63
- t = !0, I && clearInterval(I);
66
+ r = !0, g && clearInterval(g);
64
67
  };
65
- }, [e.url, e.token, e.profileId, e.refreshMs, f]), { conversations: c, loading: n, error: i, refresh: () => l((t) => t + 1) };
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
- S as useRelayChatList,
69
- E as useRelayConversation
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 $e } from "./uid.js";
6
- import { P as fe, C as he, E as we, b as be, m as xe } from "./e2e.js";
7
- import { e as Me } from "./e2e.js";
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 je(t) {
686
+ function Le(t) {
686
687
  var e;
687
688
  (e = M.get(t)) == null || e.close(), M.delete(t);
688
689
  }
689
- function Le(t) {
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 we(`ocw-e2e-${t.profileId}`);
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
- ...t.subjectId ? { subjectId: t.subjectId } : {},
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 be({
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
- be as ConnectionManager,
959
- we as E2ESession,
962
+ we as ConnectionManager,
963
+ be as E2ESession,
960
964
  fe as PersistentOutbox,
961
965
  Te as Renderer,
962
- Me as asConversationId,
966
+ qe as asConversationId,
963
967
  xe as extractX3DHInit,
964
- $e as httpBaseFromWsUrl,
965
- Le as mount,
968
+ Be as httpBaseFromWsUrl,
969
+ Ae as mount,
966
970
  pe as resolveRelayUrls,
967
971
  ue as restoreHistory,
968
- je as unmount
972
+ Le as unmount
969
973
  };
970
974
  //# sourceMappingURL=index.js.map