@thrillee/aegischat 0.1.4 → 0.1.6
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 +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +568 -327
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -327
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useChat.ts +721 -386
package/dist/index.mjs
CHANGED
|
@@ -245,15 +245,28 @@ var MAX_RECONNECT_ATTEMPTS = 5;
|
|
|
245
245
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
246
246
|
var PING_INTERVAL = 3e4;
|
|
247
247
|
var SESSION_STORAGE_KEY = "@aegischat/activeChannel";
|
|
248
|
-
function useChat(options) {
|
|
249
|
-
const {
|
|
250
|
-
|
|
248
|
+
function useChat(options = {}) {
|
|
249
|
+
const {
|
|
250
|
+
config,
|
|
251
|
+
role,
|
|
252
|
+
clientId,
|
|
253
|
+
initialSession,
|
|
254
|
+
autoConnect = true,
|
|
255
|
+
onMessage,
|
|
256
|
+
onTyping,
|
|
257
|
+
onConnectionChange
|
|
258
|
+
} = options;
|
|
259
|
+
const [session, setSession] = useState(null);
|
|
251
260
|
const [isConnected, setIsConnected] = useState(false);
|
|
252
261
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
253
|
-
const [activeChannelId, setActiveChannelIdState] = useState(
|
|
262
|
+
const [activeChannelId, setActiveChannelIdState] = useState(
|
|
263
|
+
null
|
|
264
|
+
);
|
|
254
265
|
const [channels, setChannels] = useState([]);
|
|
255
266
|
const [messages, setMessages] = useState([]);
|
|
256
|
-
const [typingUsers, setTypingUsers] = useState(
|
|
267
|
+
const [typingUsers, setTypingUsers] = useState(
|
|
268
|
+
{}
|
|
269
|
+
);
|
|
257
270
|
const [isLoadingChannels, setIsLoadingChannels] = useState(false);
|
|
258
271
|
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
|
|
259
272
|
const [hasMoreMessages, setHasMoreMessages] = useState(true);
|
|
@@ -266,17 +279,16 @@ function useChat(options) {
|
|
|
266
279
|
const isManualDisconnect = useRef(false);
|
|
267
280
|
const oldestMessageId = useRef(null);
|
|
268
281
|
const activeChannelIdRef = useRef(null);
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
282
|
+
const sessionRef = useRef(null);
|
|
283
|
+
const roleRef = useRef(void 0);
|
|
284
|
+
const clientIdRef = useRef(void 0);
|
|
285
|
+
const autoConnectRef = useRef(true);
|
|
286
|
+
const onMessageRef = useRef(void 0);
|
|
287
|
+
const onTypingRef = useRef(void 0);
|
|
288
|
+
const onConnectionChangeRef = useRef(void 0);
|
|
277
289
|
useEffect(() => {
|
|
278
|
-
|
|
279
|
-
}, [
|
|
290
|
+
activeChannelIdRef.current = activeChannelId;
|
|
291
|
+
}, [activeChannelId]);
|
|
280
292
|
useEffect(() => {
|
|
281
293
|
activeChannelIdRef.current = activeChannelId;
|
|
282
294
|
}, [activeChannelId]);
|
|
@@ -294,26 +306,29 @@ function useChat(options) {
|
|
|
294
306
|
}
|
|
295
307
|
}
|
|
296
308
|
}, []);
|
|
297
|
-
const fetchFromComms = useCallback(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const response = await fetch(`${currentSession.api_url}${path}`, {
|
|
303
|
-
...fetchOptions,
|
|
304
|
-
headers: {
|
|
305
|
-
"Content-Type": "application/json",
|
|
306
|
-
Authorization: `Bearer ${currentSession.access_token}`,
|
|
307
|
-
...fetchOptions.headers
|
|
309
|
+
const fetchFromComms = useCallback(
|
|
310
|
+
async (path, fetchOptions = {}) => {
|
|
311
|
+
const currentSession = sessionRef.current;
|
|
312
|
+
if (!currentSession) {
|
|
313
|
+
throw new Error("Chat session not initialized");
|
|
308
314
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
const response = await fetch(`${currentSession.api_url}${path}`, {
|
|
316
|
+
...fetchOptions,
|
|
317
|
+
headers: {
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
|
+
Authorization: `Bearer ${currentSession.access_token}`,
|
|
320
|
+
...fetchOptions.headers
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
const error = await response.json().catch(() => ({}));
|
|
325
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
326
|
+
}
|
|
327
|
+
const data = await response.json();
|
|
328
|
+
return data.data || data;
|
|
329
|
+
},
|
|
330
|
+
[]
|
|
331
|
+
);
|
|
317
332
|
const clearTimers = useCallback(() => {
|
|
318
333
|
if (reconnectTimeout.current) {
|
|
319
334
|
clearTimeout(reconnectTimeout.current);
|
|
@@ -324,111 +339,141 @@ function useChat(options) {
|
|
|
324
339
|
pingInterval.current = null;
|
|
325
340
|
}
|
|
326
341
|
}, []);
|
|
327
|
-
const handleWebSocketMessage = useCallback(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
342
|
+
const handleWebSocketMessage = useCallback(
|
|
343
|
+
(data) => {
|
|
344
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
345
|
+
console.log("[AegisChat] WebSocket message received:", data.type, data);
|
|
346
|
+
switch (data.type) {
|
|
347
|
+
case "message.new": {
|
|
348
|
+
const newMessage = data.payload;
|
|
349
|
+
if (newMessage.channel_id === currentActiveChannelId) {
|
|
350
|
+
setMessages((prev) => {
|
|
351
|
+
const existingIndex = prev.findIndex(
|
|
352
|
+
(m) => m.tempId && m.content === newMessage.content && m.status === "sending"
|
|
353
|
+
);
|
|
354
|
+
if (existingIndex !== -1) {
|
|
355
|
+
const updated = [...prev];
|
|
356
|
+
updated[existingIndex] = { ...newMessage, status: "sent" };
|
|
357
|
+
return updated;
|
|
358
|
+
}
|
|
359
|
+
if (prev.some((m) => m.id === newMessage.id)) return prev;
|
|
360
|
+
return [...prev, { ...newMessage, status: "delivered" }];
|
|
361
|
+
});
|
|
362
|
+
onMessageRef.current?.(newMessage);
|
|
363
|
+
}
|
|
364
|
+
setChannels((prev) => {
|
|
365
|
+
const updated = prev.map(
|
|
366
|
+
(ch) => ch.id === newMessage.channel_id ? {
|
|
367
|
+
...ch,
|
|
368
|
+
last_message: {
|
|
369
|
+
id: newMessage.id,
|
|
370
|
+
content: newMessage.content,
|
|
371
|
+
created_at: newMessage.created_at,
|
|
372
|
+
sender: {
|
|
373
|
+
id: newMessage.sender_id,
|
|
374
|
+
display_name: "Unknown",
|
|
375
|
+
status: "online"
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
|
|
379
|
+
} : ch
|
|
337
380
|
);
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
if (prev.some((m) => m.id === newMessage.id)) return prev;
|
|
344
|
-
return [...prev, { ...newMessage, status: "delivered" }];
|
|
381
|
+
return updated.sort((a, b) => {
|
|
382
|
+
const timeA = a.last_message?.created_at || "";
|
|
383
|
+
const timeB = b.last_message?.created_at || "";
|
|
384
|
+
return timeB.localeCompare(timeA);
|
|
385
|
+
});
|
|
345
386
|
});
|
|
346
|
-
|
|
387
|
+
break;
|
|
347
388
|
}
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
(ch) => ch.id === newMessage.channel_id ? {
|
|
351
|
-
...ch,
|
|
352
|
-
last_message: {
|
|
353
|
-
id: newMessage.id,
|
|
354
|
-
content: newMessage.content,
|
|
355
|
-
created_at: newMessage.created_at,
|
|
356
|
-
sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
|
|
357
|
-
},
|
|
358
|
-
unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
|
|
359
|
-
} : ch
|
|
360
|
-
);
|
|
361
|
-
return updated.sort((a, b) => {
|
|
362
|
-
const timeA = a.last_message?.created_at || "";
|
|
363
|
-
const timeB = b.last_message?.created_at || "";
|
|
364
|
-
return timeB.localeCompare(timeA);
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
case "message.updated": {
|
|
370
|
-
const updatedMessage = data.payload;
|
|
371
|
-
setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
case "message.deleted": {
|
|
375
|
-
const { message_id } = data.payload;
|
|
376
|
-
setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
case "message.delivered":
|
|
380
|
-
case "message.read": {
|
|
381
|
-
const { message_id, channel_id, status } = data.payload;
|
|
382
|
-
if (channel_id === currentActiveChannelId) {
|
|
389
|
+
case "message.updated": {
|
|
390
|
+
const updatedMessage = data.payload;
|
|
383
391
|
setMessages(
|
|
384
|
-
(prev) => prev.map((m) => m.id ===
|
|
392
|
+
(prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
|
|
385
393
|
);
|
|
394
|
+
break;
|
|
386
395
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
case "message.delivered.batch":
|
|
390
|
-
case "message.read.batch": {
|
|
391
|
-
const { channel_id } = data.payload;
|
|
392
|
-
if (channel_id === currentActiveChannelId) {
|
|
396
|
+
case "message.deleted": {
|
|
397
|
+
const { message_id } = data.payload;
|
|
393
398
|
setMessages(
|
|
394
|
-
(prev) => prev.map(
|
|
399
|
+
(prev) => prev.map(
|
|
400
|
+
(m) => m.id === message_id ? { ...m, deleted: true } : m
|
|
401
|
+
)
|
|
395
402
|
);
|
|
403
|
+
break;
|
|
396
404
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
405
|
+
case "message.delivered":
|
|
406
|
+
case "message.read": {
|
|
407
|
+
const { message_id, channel_id, status } = data.payload;
|
|
408
|
+
if (channel_id === currentActiveChannelId) {
|
|
409
|
+
setMessages(
|
|
410
|
+
(prev) => prev.map(
|
|
411
|
+
(m) => m.id === message_id ? {
|
|
412
|
+
...m,
|
|
413
|
+
status: status || "delivered"
|
|
414
|
+
} : m
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "message.delivered.batch":
|
|
421
|
+
case "message.read.batch": {
|
|
422
|
+
const { channel_id } = data.payload;
|
|
423
|
+
if (channel_id === currentActiveChannelId) {
|
|
424
|
+
setMessages(
|
|
425
|
+
(prev) => prev.map(
|
|
426
|
+
(m) => m.status === "sent" || m.status === "delivered" ? {
|
|
427
|
+
...m,
|
|
428
|
+
status: data.type === "message.delivered.batch" ? "delivered" : "read"
|
|
429
|
+
} : m
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case "typing.start": {
|
|
436
|
+
const { channel_id, user } = data.payload;
|
|
437
|
+
const typingUser = {
|
|
438
|
+
id: user.id,
|
|
439
|
+
displayName: user.display_name,
|
|
440
|
+
avatarUrl: user.avatar_url,
|
|
441
|
+
startedAt: Date.now()
|
|
442
|
+
};
|
|
443
|
+
setTypingUsers((prev) => ({
|
|
444
|
+
...prev,
|
|
445
|
+
[channel_id]: [
|
|
446
|
+
...(prev[channel_id] || []).filter((u) => u.id !== user.id),
|
|
447
|
+
typingUser
|
|
448
|
+
]
|
|
449
|
+
}));
|
|
450
|
+
onTypingRef.current?.(channel_id, typingUser);
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
case "typing.stop": {
|
|
454
|
+
const { channel_id, user_id } = data.payload;
|
|
455
|
+
setTypingUsers((prev) => ({
|
|
456
|
+
...prev,
|
|
457
|
+
[channel_id]: (prev[channel_id] || []).filter(
|
|
458
|
+
(u) => u.id !== user_id
|
|
459
|
+
)
|
|
460
|
+
}));
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case "pong":
|
|
464
|
+
break;
|
|
465
|
+
default:
|
|
466
|
+
console.log("[AegisChat] Unhandled message type:", data.type);
|
|
421
467
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
console.log("[AegisChat] Unhandled message type:", data.type);
|
|
426
|
-
}
|
|
427
|
-
}, [onMessage, onTyping]);
|
|
468
|
+
},
|
|
469
|
+
[]
|
|
470
|
+
);
|
|
428
471
|
const connectWebSocket = useCallback(() => {
|
|
429
472
|
const currentSession = sessionRef.current;
|
|
430
473
|
if (!currentSession?.websocket_url || !currentSession?.access_token) {
|
|
431
|
-
console.warn(
|
|
474
|
+
console.warn(
|
|
475
|
+
"[AegisChat] Cannot connect WebSocket - missing session or token"
|
|
476
|
+
);
|
|
432
477
|
return;
|
|
433
478
|
}
|
|
434
479
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
@@ -445,14 +490,19 @@ function useChat(options) {
|
|
|
445
490
|
setIsConnected(true);
|
|
446
491
|
setIsConnecting(false);
|
|
447
492
|
reconnectAttempts.current = 0;
|
|
448
|
-
|
|
493
|
+
onConnectionChangeRef.current?.(true);
|
|
449
494
|
pingInterval.current = setInterval(() => {
|
|
450
495
|
if (ws.readyState === WebSocket.OPEN) {
|
|
451
496
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
452
497
|
}
|
|
453
498
|
}, PING_INTERVAL);
|
|
454
499
|
if (activeChannelIdRef.current) {
|
|
455
|
-
ws.send(
|
|
500
|
+
ws.send(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
type: "channel.join",
|
|
503
|
+
payload: { channel_id: activeChannelIdRef.current }
|
|
504
|
+
})
|
|
505
|
+
);
|
|
456
506
|
}
|
|
457
507
|
};
|
|
458
508
|
ws.onmessage = (event) => {
|
|
@@ -468,9 +518,12 @@ function useChat(options) {
|
|
|
468
518
|
setIsConnected(false);
|
|
469
519
|
setIsConnecting(false);
|
|
470
520
|
clearTimers();
|
|
471
|
-
|
|
521
|
+
onConnectionChangeRef.current?.(false);
|
|
472
522
|
if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
|
|
473
|
-
const delay = Math.min(
|
|
523
|
+
const delay = Math.min(
|
|
524
|
+
RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current),
|
|
525
|
+
MAX_RECONNECT_DELAY
|
|
526
|
+
);
|
|
474
527
|
console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
|
|
475
528
|
reconnectTimeout.current = setTimeout(() => {
|
|
476
529
|
reconnectAttempts.current++;
|
|
@@ -482,14 +535,18 @@ function useChat(options) {
|
|
|
482
535
|
console.error("[AegisChat] WebSocket error:", error);
|
|
483
536
|
};
|
|
484
537
|
wsRef.current = ws;
|
|
485
|
-
}, [clearTimers, handleWebSocketMessage
|
|
538
|
+
}, [clearTimers, handleWebSocketMessage]);
|
|
486
539
|
const connect = useCallback(async () => {
|
|
487
540
|
console.log("[AegisChat] connect() called");
|
|
488
|
-
const targetSession = sessionRef.current
|
|
541
|
+
const targetSession = sessionRef.current;
|
|
489
542
|
if (!targetSession) {
|
|
490
543
|
console.log("[AegisChat] No session available, skipping connect");
|
|
491
544
|
return;
|
|
492
545
|
}
|
|
546
|
+
if (!autoConnectRef.current) {
|
|
547
|
+
console.log("[AegisChat] autoConnect is false, skipping connect");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
493
550
|
connectWebSocket();
|
|
494
551
|
}, [connectWebSocket]);
|
|
495
552
|
const disconnect = useCallback(() => {
|
|
@@ -517,48 +574,72 @@ function useChat(options) {
|
|
|
517
574
|
setIsLoadingChannels(false);
|
|
518
575
|
}
|
|
519
576
|
}, []);
|
|
520
|
-
const selectChannel = useCallback(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (
|
|
528
|
-
|
|
577
|
+
const selectChannel = useCallback(
|
|
578
|
+
async (channelId) => {
|
|
579
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
580
|
+
setActiveChannelId(channelId);
|
|
581
|
+
setMessages([]);
|
|
582
|
+
setHasMoreMessages(true);
|
|
583
|
+
oldestMessageId.current = null;
|
|
584
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
585
|
+
if (currentActiveChannelId) {
|
|
586
|
+
wsRef.current.send(
|
|
587
|
+
JSON.stringify({
|
|
588
|
+
type: "channel.leave",
|
|
589
|
+
payload: { channel_id: currentActiveChannelId }
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
wsRef.current.send(
|
|
594
|
+
JSON.stringify({
|
|
595
|
+
type: "channel.join",
|
|
596
|
+
payload: { channel_id: channelId }
|
|
597
|
+
})
|
|
598
|
+
);
|
|
529
599
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
600
|
+
setIsLoadingMessages(true);
|
|
601
|
+
try {
|
|
602
|
+
const response = await fetchFromComms(
|
|
603
|
+
`/channels/${channelId}/messages?limit=50`
|
|
604
|
+
);
|
|
605
|
+
setMessages(response.messages || []);
|
|
606
|
+
setHasMoreMessages(response.has_more);
|
|
607
|
+
if (response.oldest_id) {
|
|
608
|
+
oldestMessageId.current = response.oldest_id;
|
|
609
|
+
}
|
|
610
|
+
await markAsRead(channelId);
|
|
611
|
+
setChannels(
|
|
612
|
+
(prev) => prev.map(
|
|
613
|
+
(ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error("[AegisChat] Failed to load messages:", error);
|
|
618
|
+
setMessages([]);
|
|
619
|
+
} finally {
|
|
620
|
+
setIsLoadingMessages(false);
|
|
539
621
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
console.error("[AegisChat] Failed to mark as read:", error);
|
|
554
|
-
}
|
|
555
|
-
}, [fetchFromComms]);
|
|
622
|
+
},
|
|
623
|
+
[setActiveChannelId, fetchFromComms]
|
|
624
|
+
);
|
|
625
|
+
const markAsRead = useCallback(
|
|
626
|
+
async (channelId) => {
|
|
627
|
+
try {
|
|
628
|
+
await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error("[AegisChat] Failed to mark as read:", error);
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
[fetchFromComms]
|
|
634
|
+
);
|
|
556
635
|
const loadMoreMessages = useCallback(async () => {
|
|
557
636
|
if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
|
|
558
637
|
setIsLoadingMessages(true);
|
|
559
638
|
try {
|
|
560
639
|
const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
|
|
561
|
-
const response = await fetchFromComms(
|
|
640
|
+
const response = await fetchFromComms(
|
|
641
|
+
`/channels/${activeChannelId}/messages${params}`
|
|
642
|
+
);
|
|
562
643
|
setMessages((prev) => [...response.messages || [], ...prev]);
|
|
563
644
|
setHasMoreMessages(response.has_more);
|
|
564
645
|
if (response.oldest_id) {
|
|
@@ -570,138 +651,234 @@ function useChat(options) {
|
|
|
570
651
|
setIsLoadingMessages(false);
|
|
571
652
|
}
|
|
572
653
|
}, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
|
|
573
|
-
const sendMessage = useCallback(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
});
|
|
616
|
-
} catch (error) {
|
|
617
|
-
console.error("[AegisChat] Failed to send message:", error);
|
|
618
|
-
setMessages(
|
|
619
|
-
(prev) => prev.map(
|
|
620
|
-
(m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
|
|
621
|
-
)
|
|
622
|
-
);
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
625
|
-
}, [fetchFromComms]);
|
|
626
|
-
const uploadFile = useCallback(async (file) => {
|
|
627
|
-
const currentSession = sessionRef.current;
|
|
628
|
-
if (!currentSession) return null;
|
|
629
|
-
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
630
|
-
setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
|
|
631
|
-
try {
|
|
632
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
|
|
633
|
-
const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
|
|
634
|
-
method: "POST",
|
|
635
|
-
body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
|
|
636
|
-
});
|
|
637
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
|
|
638
|
-
const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
|
|
639
|
-
method: "PUT",
|
|
640
|
-
body: file,
|
|
641
|
-
headers: { "Content-Type": file.type || "application/octet-stream" }
|
|
642
|
-
});
|
|
643
|
-
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
644
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
|
|
645
|
-
const confirmResponse = await fetchFromComms("/files", {
|
|
646
|
-
method: "POST",
|
|
647
|
-
body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
|
|
654
|
+
const sendMessage = useCallback(
|
|
655
|
+
async (content, msgOptions = {}) => {
|
|
656
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
657
|
+
const currentSession = sessionRef.current;
|
|
658
|
+
if (!currentActiveChannelId || !content.trim() || !currentSession) return;
|
|
659
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
660
|
+
const trimmedContent = content.trim();
|
|
661
|
+
const optimisticMessage = {
|
|
662
|
+
id: tempId,
|
|
663
|
+
tempId,
|
|
664
|
+
channel_id: currentActiveChannelId,
|
|
665
|
+
sender_id: currentSession.comms_user_id,
|
|
666
|
+
content: trimmedContent,
|
|
667
|
+
type: msgOptions.type || "text",
|
|
668
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
669
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
670
|
+
status: "sending",
|
|
671
|
+
metadata: msgOptions.metadata || {}
|
|
672
|
+
};
|
|
673
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
674
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
675
|
+
setChannels((prev) => {
|
|
676
|
+
const updated = prev.map(
|
|
677
|
+
(ch) => ch.id === currentActiveChannelId ? {
|
|
678
|
+
...ch,
|
|
679
|
+
last_message: {
|
|
680
|
+
id: tempId,
|
|
681
|
+
content: trimmedContent,
|
|
682
|
+
created_at: now,
|
|
683
|
+
sender: {
|
|
684
|
+
id: currentSession.comms_user_id,
|
|
685
|
+
display_name: "You",
|
|
686
|
+
status: "online"
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} : ch
|
|
690
|
+
);
|
|
691
|
+
return updated.sort((a, b) => {
|
|
692
|
+
const timeA = a.last_message?.created_at || "";
|
|
693
|
+
const timeB = b.last_message?.created_at || "";
|
|
694
|
+
return timeB.localeCompare(timeA);
|
|
695
|
+
});
|
|
648
696
|
});
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
status: "sending",
|
|
675
|
-
metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
|
|
676
|
-
};
|
|
677
|
-
setMessages((prev) => [...prev, optimisticMessage]);
|
|
678
|
-
try {
|
|
679
|
-
const uploadedFiles = [];
|
|
680
|
-
for (const file of files) {
|
|
681
|
-
const attachment = await uploadFile(file);
|
|
682
|
-
if (attachment) uploadedFiles.push(attachment);
|
|
697
|
+
try {
|
|
698
|
+
await fetchFromComms(
|
|
699
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
700
|
+
{
|
|
701
|
+
method: "POST",
|
|
702
|
+
body: JSON.stringify({
|
|
703
|
+
content: trimmedContent,
|
|
704
|
+
type: msgOptions.type || "text",
|
|
705
|
+
parent_id: msgOptions.parent_id,
|
|
706
|
+
metadata: msgOptions.metadata
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.error("[AegisChat] Failed to send message:", error);
|
|
712
|
+
setMessages(
|
|
713
|
+
(prev) => prev.map(
|
|
714
|
+
(m) => m.tempId === tempId ? {
|
|
715
|
+
...m,
|
|
716
|
+
status: "failed",
|
|
717
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
718
|
+
} : m
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
throw error;
|
|
683
722
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
723
|
+
},
|
|
724
|
+
[fetchFromComms]
|
|
725
|
+
);
|
|
726
|
+
const uploadFile = useCallback(
|
|
727
|
+
async (file) => {
|
|
728
|
+
const currentSession = sessionRef.current;
|
|
729
|
+
if (!currentSession) return null;
|
|
730
|
+
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
731
|
+
setUploadProgress((prev) => [
|
|
732
|
+
...prev,
|
|
733
|
+
{ fileId, fileName: file.name, progress: 0, status: "pending" }
|
|
734
|
+
]);
|
|
735
|
+
try {
|
|
736
|
+
setUploadProgress(
|
|
737
|
+
(prev) => prev.map(
|
|
738
|
+
(p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
|
|
742
|
+
method: "POST",
|
|
743
|
+
body: JSON.stringify({
|
|
744
|
+
file_name: file.name,
|
|
745
|
+
file_type: file.type || "application/octet-stream",
|
|
746
|
+
file_size: file.size
|
|
747
|
+
})
|
|
748
|
+
});
|
|
749
|
+
setUploadProgress(
|
|
750
|
+
(prev) => prev.map(
|
|
751
|
+
(p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
|
|
755
|
+
method: "PUT",
|
|
756
|
+
body: file,
|
|
757
|
+
headers: { "Content-Type": file.type || "application/octet-stream" }
|
|
758
|
+
});
|
|
759
|
+
if (!uploadResponse.ok)
|
|
760
|
+
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
761
|
+
setUploadProgress(
|
|
762
|
+
(prev) => prev.map(
|
|
763
|
+
(p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p
|
|
764
|
+
)
|
|
765
|
+
);
|
|
766
|
+
const confirmResponse = await fetchFromComms(
|
|
767
|
+
"/files",
|
|
768
|
+
{
|
|
769
|
+
method: "POST",
|
|
770
|
+
body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
setUploadProgress(
|
|
774
|
+
(prev) => prev.map(
|
|
775
|
+
(p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p
|
|
776
|
+
)
|
|
777
|
+
);
|
|
778
|
+
setTimeout(
|
|
779
|
+
() => setUploadProgress(
|
|
780
|
+
(prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)
|
|
781
|
+
),
|
|
782
|
+
2e3
|
|
783
|
+
);
|
|
784
|
+
return confirmResponse.file;
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.error("[AegisChat] Failed to upload file:", error);
|
|
787
|
+
setUploadProgress(
|
|
788
|
+
(prev) => prev.map(
|
|
789
|
+
(p) => p.fileId === fileId ? {
|
|
790
|
+
...p,
|
|
791
|
+
status: "error",
|
|
792
|
+
error: error instanceof Error ? error.message : "Upload failed"
|
|
793
|
+
} : p
|
|
794
|
+
)
|
|
795
|
+
);
|
|
796
|
+
setTimeout(
|
|
797
|
+
() => setUploadProgress(
|
|
798
|
+
(prev) => prev.filter((p) => p.fileId !== fileId)
|
|
799
|
+
),
|
|
800
|
+
5e3
|
|
801
|
+
);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
[fetchFromComms]
|
|
806
|
+
);
|
|
807
|
+
const sendMessageWithFiles = useCallback(
|
|
808
|
+
async (content, files, msgOptions = {}) => {
|
|
809
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
810
|
+
const currentSession = sessionRef.current;
|
|
811
|
+
if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession)
|
|
812
|
+
return;
|
|
813
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
814
|
+
const trimmedContent = content.trim();
|
|
815
|
+
const optimisticMessage = {
|
|
816
|
+
id: tempId,
|
|
817
|
+
tempId,
|
|
818
|
+
channel_id: currentActiveChannelId,
|
|
819
|
+
sender_id: currentSession.comms_user_id,
|
|
820
|
+
content: trimmedContent || `Uploading ${files.length} file(s)...`,
|
|
821
|
+
type: "file",
|
|
822
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
823
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
824
|
+
status: "sending",
|
|
825
|
+
metadata: {
|
|
826
|
+
...msgOptions.metadata,
|
|
827
|
+
files: files.map((f) => ({
|
|
828
|
+
id: `temp-${f.name}`,
|
|
829
|
+
filename: f.name,
|
|
830
|
+
mime_type: f.type,
|
|
831
|
+
size: f.size,
|
|
832
|
+
url: ""
|
|
833
|
+
}))
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
837
|
+
try {
|
|
838
|
+
const uploadedFiles = [];
|
|
839
|
+
for (const file of files) {
|
|
840
|
+
const attachment = await uploadFile(file);
|
|
841
|
+
if (attachment) uploadedFiles.push(attachment);
|
|
842
|
+
}
|
|
843
|
+
const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
|
|
844
|
+
await fetchFromComms(
|
|
845
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
846
|
+
{
|
|
847
|
+
method: "POST",
|
|
848
|
+
body: JSON.stringify({
|
|
849
|
+
content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
|
|
850
|
+
type: msgOptions.type || messageType,
|
|
851
|
+
parent_id: msgOptions.parent_id,
|
|
852
|
+
metadata: { ...msgOptions.metadata, files: uploadedFiles },
|
|
853
|
+
file_ids: uploadedFiles.map((f) => f.id)
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.error("[AegisChat] Failed to send message with files:", error);
|
|
859
|
+
setMessages(
|
|
860
|
+
(prev) => prev.map(
|
|
861
|
+
(m) => m.tempId === tempId ? {
|
|
862
|
+
...m,
|
|
863
|
+
status: "failed",
|
|
864
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
865
|
+
} : m
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
[fetchFromComms, uploadFile]
|
|
872
|
+
);
|
|
701
873
|
const stopTyping = useCallback(() => {
|
|
702
874
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
703
875
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
704
|
-
wsRef.current.send(
|
|
876
|
+
wsRef.current.send(
|
|
877
|
+
JSON.stringify({
|
|
878
|
+
type: "typing.stop",
|
|
879
|
+
payload: { channel_id: currentActiveChannelId }
|
|
880
|
+
})
|
|
881
|
+
);
|
|
705
882
|
if (typingTimeout.current) {
|
|
706
883
|
clearTimeout(typingTimeout.current);
|
|
707
884
|
typingTimeout.current = null;
|
|
@@ -710,46 +887,106 @@ function useChat(options) {
|
|
|
710
887
|
const startTyping = useCallback(() => {
|
|
711
888
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
712
889
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
713
|
-
wsRef.current.send(
|
|
890
|
+
wsRef.current.send(
|
|
891
|
+
JSON.stringify({
|
|
892
|
+
type: "typing.start",
|
|
893
|
+
payload: { channel_id: currentActiveChannelId }
|
|
894
|
+
})
|
|
895
|
+
);
|
|
714
896
|
if (typingTimeout.current) clearTimeout(typingTimeout.current);
|
|
715
897
|
typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
|
|
716
898
|
}, [stopTyping]);
|
|
717
|
-
const createDMWithUser = useCallback(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
899
|
+
const createDMWithUser = useCallback(
|
|
900
|
+
async (userId) => {
|
|
901
|
+
try {
|
|
902
|
+
const channel = await fetchFromComms("/channels/dm", {
|
|
903
|
+
method: "POST",
|
|
904
|
+
body: JSON.stringify({ user_id: userId })
|
|
905
|
+
});
|
|
906
|
+
await refreshChannels();
|
|
907
|
+
return channel.id;
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.error("[AegisChat] Failed to create DM:", error);
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
[fetchFromComms, refreshChannels]
|
|
914
|
+
);
|
|
915
|
+
const retryMessage = useCallback(
|
|
916
|
+
async (tempId) => {
|
|
917
|
+
const failedMessage = messages.find(
|
|
918
|
+
(m) => m.tempId === tempId && m.status === "failed"
|
|
919
|
+
);
|
|
920
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
921
|
+
if (!failedMessage || !currentActiveChannelId) return;
|
|
922
|
+
setMessages(
|
|
923
|
+
(prev) => prev.map(
|
|
924
|
+
(m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m
|
|
925
|
+
)
|
|
926
|
+
);
|
|
927
|
+
try {
|
|
928
|
+
await fetchFromComms(
|
|
929
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
930
|
+
{
|
|
931
|
+
method: "POST",
|
|
932
|
+
body: JSON.stringify({
|
|
933
|
+
content: failedMessage.content,
|
|
934
|
+
type: failedMessage.type,
|
|
935
|
+
metadata: failedMessage.metadata
|
|
936
|
+
})
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error("[AegisChat] Failed to retry message:", error);
|
|
941
|
+
setMessages(
|
|
942
|
+
(prev) => prev.map(
|
|
943
|
+
(m) => m.tempId === tempId ? {
|
|
944
|
+
...m,
|
|
945
|
+
status: "failed",
|
|
946
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
947
|
+
} : m
|
|
948
|
+
)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
[messages, fetchFromComms]
|
|
953
|
+
);
|
|
745
954
|
const deleteFailedMessage = useCallback((tempId) => {
|
|
746
955
|
setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
|
|
747
956
|
}, []);
|
|
957
|
+
const setup = useCallback((options2) => {
|
|
958
|
+
const {
|
|
959
|
+
config: config2,
|
|
960
|
+
role: role2,
|
|
961
|
+
clientId: clientId2,
|
|
962
|
+
initialSession: initialSession2,
|
|
963
|
+
autoConnect: autoConnect2 = true,
|
|
964
|
+
onMessage: onMessage2,
|
|
965
|
+
onTyping: onTyping2,
|
|
966
|
+
onConnectionChange: onConnectionChange2
|
|
967
|
+
} = options2;
|
|
968
|
+
roleRef.current = role2;
|
|
969
|
+
clientIdRef.current = clientId2;
|
|
970
|
+
autoConnectRef.current = autoConnect2;
|
|
971
|
+
onMessageRef.current = onMessage2;
|
|
972
|
+
onTypingRef.current = onTyping2;
|
|
973
|
+
onConnectionChangeRef.current = onConnectionChange2;
|
|
974
|
+
if (initialSession2) {
|
|
975
|
+
sessionRef.current = initialSession2;
|
|
976
|
+
if (!config2) {
|
|
977
|
+
configureApiClient({
|
|
978
|
+
baseUrl: initialSession2.api_url,
|
|
979
|
+
getAccessToken: async () => sessionRef.current?.access_token || ""
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
setSession(initialSession2);
|
|
983
|
+
}
|
|
984
|
+
}, []);
|
|
748
985
|
useEffect(() => {
|
|
749
|
-
if (session && !isConnected && !isConnecting &&
|
|
986
|
+
if (session && !isConnected && !isConnecting && autoConnectRef.current) {
|
|
750
987
|
connectWebSocket();
|
|
751
988
|
}
|
|
752
|
-
}, [session, isConnected, isConnecting,
|
|
989
|
+
}, [session, isConnected, isConnecting, connectWebSocket]);
|
|
753
990
|
useEffect(() => {
|
|
754
991
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
755
992
|
wsRef.current.onmessage = (event) => {
|
|
@@ -757,7 +994,10 @@ function useChat(options) {
|
|
|
757
994
|
const data = JSON.parse(event.data);
|
|
758
995
|
handleWebSocketMessage(data);
|
|
759
996
|
} catch (error) {
|
|
760
|
-
console.error(
|
|
997
|
+
console.error(
|
|
998
|
+
"[AegisChat] Failed to parse WebSocket message:",
|
|
999
|
+
error
|
|
1000
|
+
);
|
|
761
1001
|
}
|
|
762
1002
|
};
|
|
763
1003
|
}
|
|
@@ -809,7 +1049,8 @@ function useChat(options) {
|
|
|
809
1049
|
createDMWithUser,
|
|
810
1050
|
retryMessage,
|
|
811
1051
|
deleteFailedMessage,
|
|
812
|
-
markAsRead
|
|
1052
|
+
markAsRead,
|
|
1053
|
+
setup
|
|
813
1054
|
};
|
|
814
1055
|
}
|
|
815
1056
|
|