@anakin824/prdg-chat-ui 0.3.0 → 0.3.1

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/index.d.mts CHANGED
@@ -249,15 +249,42 @@ declare class ChatAPI {
249
249
  }>;
250
250
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
251
251
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
252
- findOrCreateDirect(peerUserId: string): Promise<Conversation>;
253
- createGroup(title: string, members: string[]): Promise<Conversation>;
252
+ findOrCreateDirect(peerUserId: string, opts?: {
253
+ peerKind?: "internal" | "external";
254
+ }): Promise<Conversation>;
255
+ createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
254
256
  findOrCreateEntity(input: {
255
257
  title: string;
256
258
  entity_ref: string;
257
259
  entity_uuid: string;
260
+ /** prdg-chat user UUIDs */
258
261
  members?: string[];
262
+ /** Integration / HDS user ids (`ext_user_id`); resolved server-side */
263
+ external_members?: string[];
259
264
  }): Promise<Conversation>;
265
+ /**
266
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
267
+ * 404 if none exists; 403 if the user is not a member.
268
+ */
269
+ getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
270
+ /** Add members to a group or entity conversation (admin only). */
271
+ addConversationMembers(conversationId: string, body: {
272
+ user_ids?: string[];
273
+ external_user_ids?: string[];
274
+ role?: "member" | "admin";
275
+ }): Promise<{
276
+ added_user_ids: string[];
277
+ }>;
260
278
  listConversationMembers(conversationId: string): Promise<AppUser[]>;
279
+ /**
280
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
281
+ * (`DELETE .../members/external/:external_user_id`).
282
+ */
283
+ removeConversationMember(conversationId: string, by: {
284
+ userId: string;
285
+ } | {
286
+ externalUserId: string;
287
+ }): Promise<void>;
261
288
  listContacts(q?: string, limit?: number): Promise<AppUser[]>;
262
289
  sendMessage(conversationId: string, body: {
263
290
  body?: string | null;
package/dist/index.d.ts CHANGED
@@ -249,15 +249,42 @@ declare class ChatAPI {
249
249
  }>;
250
250
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
251
251
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
252
- findOrCreateDirect(peerUserId: string): Promise<Conversation>;
253
- createGroup(title: string, members: string[]): Promise<Conversation>;
252
+ findOrCreateDirect(peerUserId: string, opts?: {
253
+ peerKind?: "internal" | "external";
254
+ }): Promise<Conversation>;
255
+ createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
254
256
  findOrCreateEntity(input: {
255
257
  title: string;
256
258
  entity_ref: string;
257
259
  entity_uuid: string;
260
+ /** prdg-chat user UUIDs */
258
261
  members?: string[];
262
+ /** Integration / HDS user ids (`ext_user_id`); resolved server-side */
263
+ external_members?: string[];
259
264
  }): Promise<Conversation>;
265
+ /**
266
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
267
+ * 404 if none exists; 403 if the user is not a member.
268
+ */
269
+ getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
270
+ /** Add members to a group or entity conversation (admin only). */
271
+ addConversationMembers(conversationId: string, body: {
272
+ user_ids?: string[];
273
+ external_user_ids?: string[];
274
+ role?: "member" | "admin";
275
+ }): Promise<{
276
+ added_user_ids: string[];
277
+ }>;
260
278
  listConversationMembers(conversationId: string): Promise<AppUser[]>;
279
+ /**
280
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
281
+ * (`DELETE .../members/external/:external_user_id`).
282
+ */
283
+ removeConversationMember(conversationId: string, by: {
284
+ userId: string;
285
+ } | {
286
+ externalUserId: string;
287
+ }): Promise<void>;
261
288
  listContacts(q?: string, limit?: number): Promise<AppUser[]>;
262
289
  sendMessage(conversationId: string, body: {
263
290
  body?: string | null;
package/dist/index.js CHANGED
@@ -66,16 +66,18 @@ var ChatAPI = class {
66
66
  q.set("limit", String(limit));
67
67
  return this.json(`/conversations/${conversationId}/messages?${q}`);
68
68
  }
69
- findOrCreateDirect(peerUserId) {
69
+ findOrCreateDirect(peerUserId, opts) {
70
+ const kind = opts?.peerKind ?? "internal";
71
+ const body = kind === "external" ? { external_user_uuid: peerUserId } : { peer_user_id: peerUserId };
70
72
  return this.json("/conversations/direct", {
71
73
  method: "POST",
72
- body: JSON.stringify({ peer_user_id: peerUserId })
74
+ body: JSON.stringify(body)
73
75
  });
74
76
  }
75
- createGroup(title, members) {
77
+ createGroup(title, members, externalMembers = []) {
76
78
  return this.json("/conversations/group", {
77
79
  method: "POST",
78
- body: JSON.stringify({ title, members })
80
+ body: JSON.stringify({ title, members, external_members: externalMembers })
79
81
  });
80
82
  }
81
83
  findOrCreateEntity(input) {
@@ -85,13 +87,58 @@ var ChatAPI = class {
85
87
  title: input.title,
86
88
  entity_ref: input.entity_ref,
87
89
  entity_uuid: input.entity_uuid,
88
- members: input.members ?? []
90
+ members: input.members ?? [],
91
+ external_members: input.external_members ?? []
92
+ })
93
+ });
94
+ }
95
+ /**
96
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
97
+ * 404 if none exists; 403 if the user is not a member.
98
+ */
99
+ getConversationByEntity(entity_ref, entity_uuid) {
100
+ return this.json(
101
+ `/conversations/by-entity/ref/${encodeURIComponent(entity_ref)}/entity/${encodeURIComponent(entity_uuid)}`
102
+ );
103
+ }
104
+ /** Add members to a group or entity conversation (admin only). */
105
+ addConversationMembers(conversationId, body) {
106
+ return this.json(`/conversations/${conversationId}/members`, {
107
+ method: "POST",
108
+ body: JSON.stringify({
109
+ user_ids: body.user_ids ?? [],
110
+ external_user_ids: body.external_user_ids ?? [],
111
+ role: body.role
89
112
  })
90
113
  });
91
114
  }
92
115
  listConversationMembers(conversationId) {
93
116
  return this.json(`/conversations/${conversationId}/members`);
94
117
  }
118
+ /**
119
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
120
+ * (`DELETE .../members/external/:external_user_id`).
121
+ */
122
+ async removeConversationMember(conversationId, by) {
123
+ const path = "externalUserId" in by ? `/conversations/${conversationId}/members/external/${encodeURIComponent(by.externalUserId)}` : `/conversations/${conversationId}/members/${by.userId}`;
124
+ let res = await fetch(`${this.baseUrl}${path}`, {
125
+ method: "DELETE",
126
+ headers: this.headers()
127
+ });
128
+ if (res.status === 401 && this.onAuthError) {
129
+ const newToken = await this.onAuthError();
130
+ if (newToken) {
131
+ res = await fetch(`${this.baseUrl}${path}`, {
132
+ method: "DELETE",
133
+ headers: this.headers(newToken)
134
+ });
135
+ }
136
+ }
137
+ if (!res.ok) {
138
+ const err = await res.text();
139
+ throw new Error(err || res.statusText);
140
+ }
141
+ }
95
142
  listContacts(q, limit = 50) {
96
143
  const params = new URLSearchParams();
97
144
  params.set("limit", String(limit));
@@ -180,22 +227,118 @@ function userInboxSubject(natsTenantId, userId) {
180
227
  function entityConversationSubject(natsTenantId, conversationId) {
181
228
  return `chat.${natsTenantId}.conversation.${conversationId}`;
182
229
  }
230
+ function pushMessageToCache(queryClient, msg) {
231
+ let inserted = false;
232
+ queryClient.setQueryData(
233
+ chatKeys.messages(msg.conversation_id),
234
+ (prev) => {
235
+ if (!prev) return void 0;
236
+ if (prev.items.some((m) => m.id === msg.id)) return prev;
237
+ inserted = true;
238
+ return { ...prev, items: [msg, ...prev.items] };
239
+ }
240
+ );
241
+ return inserted;
242
+ }
243
+ function markDeletedInCache(queryClient, conversationId, messageId, deletedAt) {
244
+ queryClient.setQueryData(
245
+ chatKeys.messages(conversationId),
246
+ (prev) => {
247
+ if (!prev) return void 0;
248
+ const idx = prev.items.findIndex((m) => m.id === messageId);
249
+ if (idx === -1) return prev;
250
+ const updated = [...prev.items];
251
+ updated[idx] = { ...updated[idx], deleted_at: deletedAt };
252
+ return { ...prev, items: updated };
253
+ }
254
+ );
255
+ }
256
+ function applyEditInCache(queryClient, updated) {
257
+ queryClient.setQueryData(
258
+ chatKeys.messages(updated.conversation_id),
259
+ (prev) => {
260
+ if (!prev) return void 0;
261
+ const idx = prev.items.findIndex((m) => m.id === updated.id);
262
+ if (idx === -1) return prev;
263
+ const items = [...prev.items];
264
+ items[idx] = { ...items[idx], ...updated };
265
+ return { ...prev, items };
266
+ }
267
+ );
268
+ }
183
269
  function invalidateQueriesFromNatsPayload(payload, queryClient) {
184
270
  if (!payload || typeof payload !== "object") return;
185
271
  const p = payload;
186
- if (p.type !== "chat.message.new") return;
272
+ const eventType = p.type;
273
+ if (typeof eventType !== "string") return;
187
274
  const ed = p.event_data;
188
275
  if (!ed || typeof ed !== "object") return;
189
- const convId = ed.conversation_id;
190
- if (typeof convId !== "string" || !convId.trim()) return;
191
- void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
192
- void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
276
+ const eventData = ed;
277
+ if (eventType === "chat.message.new") {
278
+ const convId = eventData.conversation_id;
279
+ if (typeof convId !== "string" || !convId.trim()) return;
280
+ const msgRaw = eventData.message;
281
+ if (msgRaw && typeof msgRaw === "object") {
282
+ const raw = msgRaw;
283
+ if (typeof raw.id === "string" && typeof raw.conversation_id === "string") {
284
+ const msg = {
285
+ id: raw.id,
286
+ conversation_id: raw.conversation_id,
287
+ sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
288
+ body: typeof raw.body === "string" ? raw.body : null,
289
+ created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
290
+ edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
291
+ deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
292
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
293
+ mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
294
+ };
295
+ pushMessageToCache(queryClient, msg);
296
+ }
297
+ }
298
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
299
+ void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
300
+ return;
301
+ }
302
+ if (eventType === "chat.message.edited") {
303
+ const msgRaw = eventData.message;
304
+ if (msgRaw && typeof msgRaw === "object") {
305
+ const raw = msgRaw;
306
+ if (typeof raw.id === "string" && typeof raw.conversation_id === "string") {
307
+ const updated = {
308
+ id: raw.id,
309
+ conversation_id: raw.conversation_id,
310
+ sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
311
+ body: typeof raw.body === "string" ? raw.body : null,
312
+ created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
313
+ edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
314
+ deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
315
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
316
+ mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
317
+ };
318
+ applyEditInCache(queryClient, updated);
319
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(updated.conversation_id) });
320
+ }
321
+ }
322
+ return;
323
+ }
324
+ if (eventType === "chat.message.deleted") {
325
+ const msgRaw = eventData.message;
326
+ if (msgRaw && typeof msgRaw === "object") {
327
+ const raw = msgRaw;
328
+ const msgId = raw.id;
329
+ const convId = raw.conversation_id;
330
+ const deletedAt = typeof raw.deleted_at === "string" ? raw.deleted_at : (/* @__PURE__ */ new Date()).toISOString();
331
+ if (typeof msgId === "string" && typeof convId === "string") {
332
+ markDeletedInCache(queryClient, convId, msgId, deletedAt);
333
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
334
+ }
335
+ }
336
+ }
193
337
  }
194
338
 
195
339
  // src/chat/provider/ChatNatsBridge.tsx
196
340
  function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
197
- const { natsTenantId, userId } = useChat();
198
- const queryClient = reactQuery.useQueryClient();
341
+ const { natsTenantId, userId, queryClient } = useChat();
199
342
  const { data: convData } = useConversations();
200
343
  const entityConversationIds = react.useMemo(() => {
201
344
  const items = convData?.items ?? [];
@@ -247,6 +390,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
247
390
  if (payload && typeof payload === "object" && payload.type === "chat.message.new") {
248
391
  const p = payload;
249
392
  const ed = p.event_data;
393
+ const msgRaw = ed?.message;
250
394
  const rawTs = typeof ed?.timestamp_utc === "string" ? ed.timestamp_utc : null;
251
395
  const localTime = rawTs ? new Date(rawTs).toLocaleTimeString(void 0, {
252
396
  hour: "numeric",
@@ -261,6 +405,8 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
261
405
  `
262
406
  conversation: ${typeof ed?.conversation_id === "string" ? ed.conversation_id : "?"}`,
263
407
  `
408
+ message_id: ${typeof msgRaw?.id === "string" ? msgRaw.id : "(no id \u2014 cache push skipped)"}`,
409
+ `
264
410
  sender: ${typeof ed?.sender_id === "string" ? ed.sender_id : "?"}`,
265
411
  `
266
412
  body: ${typeof ed?.body === "string" ? ed.body.slice(0, 200) : "(no body)"}`,