@thrillee/aegischat 0.1.0

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.js ADDED
@@ -0,0 +1,1065 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ channelsApi: () => channelsApi,
24
+ chatApi: () => chatApi,
25
+ configureApiClient: () => configureApiClient,
26
+ filesApi: () => filesApi,
27
+ messagesApi: () => messagesApi,
28
+ reactionsApi: () => reactionsApi,
29
+ useAutoRead: () => useAutoRead,
30
+ useChannels: () => useChannels,
31
+ useChat: () => useChat,
32
+ useFileUpload: () => useFileUpload,
33
+ useMentions: () => useMentions,
34
+ useMessages: () => useMessages,
35
+ useReactions: () => useReactions,
36
+ useTypingIndicator: () => useTypingIndicator,
37
+ usersApi: () => usersApi
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/hooks/useChat.ts
42
+ var import_react = require("react");
43
+
44
+ // src/services/api.ts
45
+ var baseUrl = "";
46
+ var getAccessToken = () => "";
47
+ var onUnauthorized;
48
+ function configureApiClient(config) {
49
+ baseUrl = config.baseUrl;
50
+ getAccessToken = config.getAccessToken;
51
+ onUnauthorized = config.onUnauthorized;
52
+ }
53
+ async function fetchWithAuth(path, options = {}) {
54
+ const token = await (typeof getAccessToken === "function" ? getAccessToken() : getAccessToken);
55
+ const response = await fetch(`${baseUrl}${path}`, {
56
+ ...options,
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ Authorization: `Bearer ${token}`,
60
+ ...options.headers
61
+ }
62
+ });
63
+ if (response.status === 401) {
64
+ onUnauthorized?.();
65
+ throw new Error("Unauthorized");
66
+ }
67
+ if (!response.ok) {
68
+ const error = await response.json().catch(() => ({}));
69
+ throw new Error(error.message || `HTTP ${response.status}`);
70
+ }
71
+ return response.json();
72
+ }
73
+ var chatApi = {
74
+ /**
75
+ * Connect to chat session
76
+ */
77
+ async connect(params, signal) {
78
+ return fetchWithAuth("/api/v1/chat/connect", {
79
+ method: "POST",
80
+ body: JSON.stringify(params),
81
+ signal
82
+ });
83
+ },
84
+ /**
85
+ * Refresh access token
86
+ */
87
+ async refreshToken(refreshToken, signal) {
88
+ return fetchWithAuth("/api/v1/chat/refresh", {
89
+ method: "POST",
90
+ body: JSON.stringify({ refresh_token: refreshToken }),
91
+ signal
92
+ });
93
+ }
94
+ };
95
+ var channelsApi = {
96
+ /**
97
+ * List channels
98
+ */
99
+ async list(options = {}, signal) {
100
+ const params = new URLSearchParams();
101
+ if (options.type) params.append("type", options.type);
102
+ if (options.limit) params.append("limit", String(options.limit));
103
+ const query = params.toString() ? `?${params.toString()}` : "";
104
+ return fetchWithAuth(`/api/v1/channels${query}`, { signal });
105
+ },
106
+ /**
107
+ * Get channel by ID
108
+ */
109
+ async get(channelId, signal) {
110
+ return fetchWithAuth(`/api/v1/channels/${channelId}`, { signal });
111
+ },
112
+ /**
113
+ * Get or create DM channel
114
+ */
115
+ async getOrCreateDM(userId, signal) {
116
+ return fetchWithAuth("/api/v1/channels/dm", {
117
+ method: "POST",
118
+ body: JSON.stringify({ user_id: userId }),
119
+ signal
120
+ });
121
+ },
122
+ /**
123
+ * Create channel
124
+ */
125
+ async create(data, signal) {
126
+ return fetchWithAuth("/api/v1/channels", {
127
+ method: "POST",
128
+ body: JSON.stringify(data),
129
+ signal
130
+ });
131
+ },
132
+ /**
133
+ * Mark channel as read
134
+ */
135
+ async markAsRead(channelId, signal) {
136
+ return fetchWithAuth(`/api/v1/channels/${channelId}/read`, {
137
+ method: "POST",
138
+ signal
139
+ });
140
+ },
141
+ /**
142
+ * Get channel members
143
+ */
144
+ async getMembers(channelId, signal) {
145
+ return fetchWithAuth(`/api/v1/channels/${channelId}/members`, { signal });
146
+ },
147
+ /**
148
+ * Update channel
149
+ */
150
+ async update(channelId, data, signal) {
151
+ return fetchWithAuth(`/api/v1/channels/${channelId}`, {
152
+ method: "PATCH",
153
+ body: JSON.stringify(data),
154
+ signal
155
+ });
156
+ }
157
+ };
158
+ var messagesApi = {
159
+ /**
160
+ * List messages in a channel
161
+ */
162
+ async list(channelId, options = {}, signal) {
163
+ const params = new URLSearchParams();
164
+ if (options.limit) params.append("limit", String(options.limit));
165
+ if (options.before) params.append("before", options.before);
166
+ const query = params.toString() ? `?${params.toString()}` : "";
167
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages${query}`, { signal });
168
+ },
169
+ /**
170
+ * Send a message
171
+ */
172
+ async send(channelId, data, signal) {
173
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages`, {
174
+ method: "POST",
175
+ body: JSON.stringify(data),
176
+ signal
177
+ });
178
+ },
179
+ /**
180
+ * Update a message
181
+ */
182
+ async update(channelId, messageId, data, signal) {
183
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages/${messageId}`, {
184
+ method: "PATCH",
185
+ body: JSON.stringify(data),
186
+ signal
187
+ });
188
+ },
189
+ /**
190
+ * Delete a message
191
+ */
192
+ async delete(channelId, messageId, signal) {
193
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages/${messageId}`, {
194
+ method: "DELETE",
195
+ signal
196
+ });
197
+ },
198
+ /**
199
+ * Mark messages as delivered
200
+ */
201
+ async markDelivered(channelId, signal) {
202
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages/delivered`, {
203
+ method: "POST",
204
+ signal
205
+ });
206
+ },
207
+ /**
208
+ * Mark messages as read
209
+ */
210
+ async markRead(channelId, signal) {
211
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages/read`, {
212
+ method: "POST",
213
+ signal
214
+ });
215
+ }
216
+ };
217
+ var reactionsApi = {
218
+ /**
219
+ * Add reaction to a message
220
+ */
221
+ async add(channelId, messageId, emoji, signal) {
222
+ return fetchWithAuth(`/api/v1/channels/${channelId}/messages/${messageId}/reactions`, {
223
+ method: "POST",
224
+ body: JSON.stringify({ emoji }),
225
+ signal
226
+ });
227
+ },
228
+ /**
229
+ * Remove reaction from a message
230
+ */
231
+ async remove(channelId, messageId, emoji, signal) {
232
+ return fetchWithAuth(
233
+ `/api/v1/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`,
234
+ { method: "DELETE", signal }
235
+ );
236
+ }
237
+ };
238
+ var filesApi = {
239
+ /**
240
+ * Get upload URL
241
+ */
242
+ async getUploadUrl(data, signal) {
243
+ return fetchWithAuth("/api/v1/files/upload-url", {
244
+ method: "POST",
245
+ body: JSON.stringify(data),
246
+ signal
247
+ });
248
+ },
249
+ /**
250
+ * Confirm file upload
251
+ */
252
+ async confirm(fileId, signal) {
253
+ return fetchWithAuth("/api/v1/files", {
254
+ method: "POST",
255
+ body: JSON.stringify({ file_id: fileId }),
256
+ signal
257
+ });
258
+ },
259
+ /**
260
+ * Get download URL
261
+ */
262
+ async getDownloadUrl(fileId, signal) {
263
+ return fetchWithAuth(`/api/v1/files/${fileId}/download`, { signal });
264
+ }
265
+ };
266
+ var usersApi = {
267
+ /**
268
+ * Search users
269
+ */
270
+ async search(query, signal) {
271
+ return fetchWithAuth(`/api/v1/users/search?q=${encodeURIComponent(query)}`, { signal });
272
+ },
273
+ /**
274
+ * Get user by ID
275
+ */
276
+ async get(userId, signal) {
277
+ return fetchWithAuth(`/api/v1/users/${userId}`, { signal });
278
+ }
279
+ };
280
+
281
+ // src/hooks/useChat.ts
282
+ var TYPING_TIMEOUT = 3e3;
283
+ var RECONNECT_INTERVAL = 3e3;
284
+ var MAX_RECONNECT_ATTEMPTS = 5;
285
+ var MAX_RECONNECT_DELAY = 3e4;
286
+ var PING_INTERVAL = 3e4;
287
+ var SESSION_STORAGE_KEY = "@aegischat/activeChannel";
288
+ function useChat(options) {
289
+ const { config, role, clientId, autoConnect = true, onMessage, onTyping, onConnectionChange } = options;
290
+ const [session, setSession] = (0, import_react.useState)(null);
291
+ const [isConnected, setIsConnected] = (0, import_react.useState)(false);
292
+ const [isConnecting, setIsConnecting] = (0, import_react.useState)(false);
293
+ const [activeChannelId, setActiveChannelIdState] = (0, import_react.useState)(null);
294
+ const [channels, setChannels] = (0, import_react.useState)([]);
295
+ const [messages, setMessages] = (0, import_react.useState)([]);
296
+ const [typingUsers, setTypingUsers] = (0, import_react.useState)({});
297
+ const [isLoadingChannels, setIsLoadingChannels] = (0, import_react.useState)(false);
298
+ const [isLoadingMessages, setIsLoadingMessages] = (0, import_react.useState)(false);
299
+ const [hasMoreMessages, setHasMoreMessages] = (0, import_react.useState)(true);
300
+ const [uploadProgress, setUploadProgress] = (0, import_react.useState)([]);
301
+ const wsRef = (0, import_react.useRef)(null);
302
+ const reconnectAttempts = (0, import_react.useRef)(0);
303
+ const reconnectTimeout = (0, import_react.useRef)(null);
304
+ const pingInterval = (0, import_react.useRef)(null);
305
+ const typingTimeout = (0, import_react.useRef)(null);
306
+ const isManualDisconnect = (0, import_react.useRef)(false);
307
+ const oldestMessageId = (0, import_react.useRef)(null);
308
+ const activeChannelIdRef = (0, import_react.useRef)(null);
309
+ const configRef = (0, import_react.useRef)(config);
310
+ const sessionRef = (0, import_react.useRef)(null);
311
+ (0, import_react.useEffect)(() => {
312
+ configRef.current = config;
313
+ }, [config]);
314
+ (0, import_react.useEffect)(() => {
315
+ activeChannelIdRef.current = activeChannelId;
316
+ }, [activeChannelId]);
317
+ (0, import_react.useEffect)(() => {
318
+ sessionRef.current = session;
319
+ }, [session]);
320
+ const getActiveChannelId = (0, import_react.useCallback)(() => {
321
+ if (typeof window === "undefined") return null;
322
+ return sessionStorage.getItem(SESSION_STORAGE_KEY);
323
+ }, []);
324
+ const setActiveChannelId = (0, import_react.useCallback)((id) => {
325
+ setActiveChannelIdState(id);
326
+ if (typeof window !== "undefined") {
327
+ if (id) {
328
+ sessionStorage.setItem(SESSION_STORAGE_KEY, id);
329
+ } else {
330
+ sessionStorage.removeItem(SESSION_STORAGE_KEY);
331
+ }
332
+ }
333
+ }, []);
334
+ const fetchFromComms = (0, import_react.useCallback)(async (path, fetchOptions = {}) => {
335
+ const currentSession = sessionRef.current;
336
+ if (!currentSession) {
337
+ throw new Error("Chat session not initialized");
338
+ }
339
+ const response = await fetch(`${currentSession.api_url}${path}`, {
340
+ ...fetchOptions,
341
+ headers: {
342
+ "Content-Type": "application/json",
343
+ Authorization: `Bearer ${currentSession.access_token}`,
344
+ ...fetchOptions.headers
345
+ }
346
+ });
347
+ if (!response.ok) {
348
+ const error = await response.json().catch(() => ({}));
349
+ throw new Error(error.message || `HTTP ${response.status}`);
350
+ }
351
+ const data = await response.json();
352
+ return data.data || data;
353
+ }, []);
354
+ const clearTimers = (0, import_react.useCallback)(() => {
355
+ if (reconnectTimeout.current) {
356
+ clearTimeout(reconnectTimeout.current);
357
+ reconnectTimeout.current = null;
358
+ }
359
+ if (pingInterval.current) {
360
+ clearInterval(pingInterval.current);
361
+ pingInterval.current = null;
362
+ }
363
+ }, []);
364
+ const handleWebSocketMessage = (0, import_react.useCallback)((data) => {
365
+ const currentActiveChannelId = activeChannelIdRef.current;
366
+ console.log("[AegisChat] WebSocket message received:", data.type, data);
367
+ switch (data.type) {
368
+ case "message.new": {
369
+ const newMessage = data.payload;
370
+ if (newMessage.channel_id === currentActiveChannelId) {
371
+ setMessages((prev) => {
372
+ const existingIndex = prev.findIndex(
373
+ (m) => m.tempId && m.content === newMessage.content && m.status === "sending"
374
+ );
375
+ if (existingIndex !== -1) {
376
+ const updated = [...prev];
377
+ updated[existingIndex] = { ...newMessage, status: "sent" };
378
+ return updated;
379
+ }
380
+ if (prev.some((m) => m.id === newMessage.id)) return prev;
381
+ return [...prev, { ...newMessage, status: "delivered" }];
382
+ });
383
+ onMessage?.(newMessage);
384
+ }
385
+ setChannels((prev) => {
386
+ const updated = prev.map(
387
+ (ch) => ch.id === newMessage.channel_id ? {
388
+ ...ch,
389
+ last_message: {
390
+ id: newMessage.id,
391
+ content: newMessage.content,
392
+ created_at: newMessage.created_at,
393
+ sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
394
+ },
395
+ unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
396
+ } : ch
397
+ );
398
+ return updated.sort((a, b) => {
399
+ const timeA = a.last_message?.created_at || "";
400
+ const timeB = b.last_message?.created_at || "";
401
+ return timeB.localeCompare(timeA);
402
+ });
403
+ });
404
+ break;
405
+ }
406
+ case "message.updated": {
407
+ const updatedMessage = data.payload;
408
+ setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
409
+ break;
410
+ }
411
+ case "message.deleted": {
412
+ const { message_id } = data.payload;
413
+ setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
414
+ break;
415
+ }
416
+ case "message.delivered":
417
+ case "message.read": {
418
+ const { message_id, channel_id, status } = data.payload;
419
+ if (channel_id === currentActiveChannelId) {
420
+ setMessages(
421
+ (prev) => prev.map((m) => m.id === message_id ? { ...m, status: status || "delivered" } : m)
422
+ );
423
+ }
424
+ break;
425
+ }
426
+ case "message.delivered.batch":
427
+ case "message.read.batch": {
428
+ const { channel_id } = data.payload;
429
+ if (channel_id === currentActiveChannelId) {
430
+ setMessages(
431
+ (prev) => prev.map((m) => m.status === "sent" || m.status === "delivered" ? { ...m, status: data.type === "message.delivered.batch" ? "delivered" : "read" } : m)
432
+ );
433
+ }
434
+ break;
435
+ }
436
+ case "typing.start": {
437
+ const { channel_id, user } = data.payload;
438
+ const typingUser = {
439
+ id: user.id,
440
+ displayName: user.display_name,
441
+ avatarUrl: user.avatar_url,
442
+ startedAt: Date.now()
443
+ };
444
+ setTypingUsers((prev) => ({
445
+ ...prev,
446
+ [channel_id]: [...(prev[channel_id] || []).filter((u) => u.id !== user.id), typingUser]
447
+ }));
448
+ onTyping?.(channel_id, typingUser);
449
+ break;
450
+ }
451
+ case "typing.stop": {
452
+ const { channel_id, user_id } = data.payload;
453
+ setTypingUsers((prev) => ({
454
+ ...prev,
455
+ [channel_id]: (prev[channel_id] || []).filter((u) => u.id !== user_id)
456
+ }));
457
+ break;
458
+ }
459
+ case "pong":
460
+ break;
461
+ default:
462
+ console.log("[AegisChat] Unhandled message type:", data.type);
463
+ }
464
+ }, [onMessage, onTyping]);
465
+ const connectWebSocket = (0, import_react.useCallback)(() => {
466
+ const currentSession = sessionRef.current;
467
+ if (!currentSession?.websocket_url || !currentSession?.access_token) {
468
+ console.warn("[AegisChat] Cannot connect WebSocket - missing session or token");
469
+ return;
470
+ }
471
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
472
+ console.log("[AegisChat] WebSocket already open, skipping connection");
473
+ return;
474
+ }
475
+ setIsConnecting(true);
476
+ isManualDisconnect.current = false;
477
+ const wsUrl = `${currentSession.websocket_url}?token=${currentSession.access_token}`;
478
+ console.log("[AegisChat] Creating WebSocket connection to:", wsUrl);
479
+ const ws = new WebSocket(wsUrl);
480
+ ws.onopen = () => {
481
+ console.log("[AegisChat] WebSocket connected");
482
+ setIsConnected(true);
483
+ reconnectAttempts.current = 0;
484
+ onConnectionChange?.(true);
485
+ pingInterval.current = setInterval(() => {
486
+ if (ws.readyState === WebSocket.OPEN) {
487
+ ws.send(JSON.stringify({ type: "ping" }));
488
+ }
489
+ }, PING_INTERVAL);
490
+ if (activeChannelIdRef.current) {
491
+ ws.send(JSON.stringify({ type: "channel.join", payload: { channel_id: activeChannelIdRef.current } }));
492
+ }
493
+ };
494
+ ws.onmessage = (event) => {
495
+ try {
496
+ const data = JSON.parse(event.data);
497
+ handleWebSocketMessage(data);
498
+ } catch (error) {
499
+ console.error("[AegisChat] Failed to parse WebSocket message:", error);
500
+ }
501
+ };
502
+ ws.onclose = () => {
503
+ console.log("[AegisChat] WebSocket disconnected");
504
+ setIsConnected(false);
505
+ clearTimers();
506
+ onConnectionChange?.(false);
507
+ if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
508
+ const delay = Math.min(RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current), MAX_RECONNECT_DELAY);
509
+ console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
510
+ reconnectTimeout.current = setTimeout(() => {
511
+ reconnectAttempts.current++;
512
+ connectWebSocket();
513
+ }, delay);
514
+ }
515
+ };
516
+ ws.onerror = (error) => {
517
+ console.error("[AegisChat] WebSocket error:", error);
518
+ };
519
+ wsRef.current = ws;
520
+ }, [clearTimers, handleWebSocketMessage, onConnectionChange]);
521
+ const connect = (0, import_react.useCallback)(async () => {
522
+ console.log("[AegisChat] connect() called");
523
+ if (sessionRef.current) {
524
+ console.log("[AegisChat] Session exists, calling connectWebSocket directly");
525
+ connectWebSocket();
526
+ return;
527
+ }
528
+ try {
529
+ setIsConnecting(true);
530
+ console.log("[AegisChat] Fetching chat session...");
531
+ const result = await chatApi.connect({ role, client_id: clientId });
532
+ console.log("[AegisChat] Chat session received:", result);
533
+ setSession(result.data);
534
+ setIsConnecting(false);
535
+ } catch (error) {
536
+ console.error("[AegisChat] Failed to get chat session:", error);
537
+ setIsConnecting(false);
538
+ throw error;
539
+ }
540
+ }, [role, clientId, connectWebSocket]);
541
+ const disconnect = (0, import_react.useCallback)(() => {
542
+ isManualDisconnect.current = true;
543
+ clearTimers();
544
+ if (wsRef.current) {
545
+ wsRef.current.close();
546
+ wsRef.current = null;
547
+ }
548
+ setIsConnected(false);
549
+ setSession(null);
550
+ setChannels([]);
551
+ setMessages([]);
552
+ }, [clearTimers]);
553
+ const refreshChannels = (0, import_react.useCallback)(async () => {
554
+ const currentSession = sessionRef.current;
555
+ if (!currentSession) return;
556
+ setIsLoadingChannels(true);
557
+ try {
558
+ const response = await channelsApi.list({});
559
+ setChannels(response.data.channels || []);
560
+ } catch (error) {
561
+ console.error("[AegisChat] Failed to fetch channels:", error);
562
+ } finally {
563
+ setIsLoadingChannels(false);
564
+ }
565
+ }, []);
566
+ const selectChannel = (0, import_react.useCallback)(async (channelId) => {
567
+ const currentActiveChannelId = activeChannelIdRef.current;
568
+ setActiveChannelId(channelId);
569
+ setMessages([]);
570
+ setHasMoreMessages(true);
571
+ oldestMessageId.current = null;
572
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
573
+ if (currentActiveChannelId) {
574
+ wsRef.current.send(JSON.stringify({ type: "channel.leave", payload: { channel_id: currentActiveChannelId } }));
575
+ }
576
+ wsRef.current.send(JSON.stringify({ type: "channel.join", payload: { channel_id: channelId } }));
577
+ }
578
+ setIsLoadingMessages(true);
579
+ try {
580
+ const response = await fetchFromComms(`/channels/${channelId}/messages?limit=50`);
581
+ setMessages(response.messages || []);
582
+ setHasMoreMessages(response.has_more);
583
+ if (response.oldest_id) {
584
+ oldestMessageId.current = response.oldest_id;
585
+ }
586
+ await markAsRead(channelId);
587
+ setChannels((prev) => prev.map((ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch));
588
+ } catch (error) {
589
+ console.error("[AegisChat] Failed to load messages:", error);
590
+ setMessages([]);
591
+ } finally {
592
+ setIsLoadingMessages(false);
593
+ }
594
+ }, [setActiveChannelId, fetchFromComms]);
595
+ const markAsRead = (0, import_react.useCallback)(async (channelId) => {
596
+ try {
597
+ await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
598
+ } catch (error) {
599
+ console.error("[AegisChat] Failed to mark as read:", error);
600
+ }
601
+ }, [fetchFromComms]);
602
+ const loadMoreMessages = (0, import_react.useCallback)(async () => {
603
+ if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
604
+ setIsLoadingMessages(true);
605
+ try {
606
+ const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
607
+ const response = await fetchFromComms(`/channels/${activeChannelId}/messages${params}`);
608
+ setMessages((prev) => [...response.messages || [], ...prev]);
609
+ setHasMoreMessages(response.has_more);
610
+ if (response.oldest_id) {
611
+ oldestMessageId.current = response.oldest_id;
612
+ }
613
+ } catch (error) {
614
+ console.error("[AegisChat] Failed to load more messages:", error);
615
+ } finally {
616
+ setIsLoadingMessages(false);
617
+ }
618
+ }, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
619
+ const sendMessage = (0, import_react.useCallback)(async (content, msgOptions = {}) => {
620
+ const currentActiveChannelId = activeChannelIdRef.current;
621
+ const currentSession = sessionRef.current;
622
+ if (!currentActiveChannelId || !content.trim() || !currentSession) return;
623
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
624
+ const trimmedContent = content.trim();
625
+ const optimisticMessage = {
626
+ id: tempId,
627
+ tempId,
628
+ channel_id: currentActiveChannelId,
629
+ sender_id: currentSession.comms_user_id,
630
+ content: trimmedContent,
631
+ type: msgOptions.type || "text",
632
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
633
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
634
+ status: "sending",
635
+ metadata: msgOptions.metadata || {}
636
+ };
637
+ setMessages((prev) => [...prev, optimisticMessage]);
638
+ const now = (/* @__PURE__ */ new Date()).toISOString();
639
+ setChannels((prev) => {
640
+ const updated = prev.map(
641
+ (ch) => ch.id === currentActiveChannelId ? {
642
+ ...ch,
643
+ last_message: {
644
+ id: tempId,
645
+ content: trimmedContent,
646
+ created_at: now,
647
+ sender: { id: currentSession.comms_user_id, display_name: "You", status: "online" }
648
+ }
649
+ } : ch
650
+ );
651
+ return updated.sort((a, b) => {
652
+ const timeA = a.last_message?.created_at || "";
653
+ const timeB = b.last_message?.created_at || "";
654
+ return timeB.localeCompare(timeA);
655
+ });
656
+ });
657
+ try {
658
+ await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
659
+ method: "POST",
660
+ body: JSON.stringify({ content: trimmedContent, type: msgOptions.type || "text", parent_id: msgOptions.parent_id, metadata: msgOptions.metadata })
661
+ });
662
+ } catch (error) {
663
+ console.error("[AegisChat] Failed to send message:", error);
664
+ setMessages(
665
+ (prev) => prev.map(
666
+ (m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
667
+ )
668
+ );
669
+ throw error;
670
+ }
671
+ }, [fetchFromComms]);
672
+ const uploadFile = (0, import_react.useCallback)(async (file) => {
673
+ const currentSession = sessionRef.current;
674
+ if (!currentSession) return null;
675
+ const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
676
+ setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
677
+ try {
678
+ setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
679
+ const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
680
+ method: "POST",
681
+ body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
682
+ });
683
+ setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
684
+ const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
685
+ method: "PUT",
686
+ body: file,
687
+ headers: { "Content-Type": file.type || "application/octet-stream" }
688
+ });
689
+ if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
690
+ setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
691
+ const confirmResponse = await fetchFromComms("/files", {
692
+ method: "POST",
693
+ body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
694
+ });
695
+ setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p));
696
+ setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)), 2e3);
697
+ return confirmResponse.file;
698
+ } catch (error) {
699
+ console.error("[AegisChat] Failed to upload file:", error);
700
+ setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "error", error: error instanceof Error ? error.message : "Upload failed" } : p));
701
+ setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== fileId)), 5e3);
702
+ return null;
703
+ }
704
+ }, [fetchFromComms]);
705
+ const sendMessageWithFiles = (0, import_react.useCallback)(async (content, files, msgOptions = {}) => {
706
+ const currentActiveChannelId = activeChannelIdRef.current;
707
+ const currentSession = sessionRef.current;
708
+ if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession) return;
709
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
710
+ const trimmedContent = content.trim();
711
+ const optimisticMessage = {
712
+ id: tempId,
713
+ tempId,
714
+ channel_id: currentActiveChannelId,
715
+ sender_id: currentSession.comms_user_id,
716
+ content: trimmedContent || `Uploading ${files.length} file(s)...`,
717
+ type: "file",
718
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
719
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
720
+ status: "sending",
721
+ metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
722
+ };
723
+ setMessages((prev) => [...prev, optimisticMessage]);
724
+ try {
725
+ const uploadedFiles = [];
726
+ for (const file of files) {
727
+ const attachment = await uploadFile(file);
728
+ if (attachment) uploadedFiles.push(attachment);
729
+ }
730
+ const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
731
+ await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
732
+ method: "POST",
733
+ body: JSON.stringify({
734
+ content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
735
+ type: msgOptions.type || messageType,
736
+ parent_id: msgOptions.parent_id,
737
+ metadata: { ...msgOptions.metadata, files: uploadedFiles },
738
+ file_ids: uploadedFiles.map((f) => f.id)
739
+ })
740
+ });
741
+ } catch (error) {
742
+ console.error("[AegisChat] Failed to send message with files:", error);
743
+ setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
744
+ throw error;
745
+ }
746
+ }, [fetchFromComms, uploadFile]);
747
+ const stopTyping = (0, import_react.useCallback)(() => {
748
+ const currentActiveChannelId = activeChannelIdRef.current;
749
+ if (!currentActiveChannelId || !wsRef.current) return;
750
+ wsRef.current.send(JSON.stringify({ type: "typing.stop", payload: { channel_id: currentActiveChannelId } }));
751
+ if (typingTimeout.current) {
752
+ clearTimeout(typingTimeout.current);
753
+ typingTimeout.current = null;
754
+ }
755
+ }, []);
756
+ const startTyping = (0, import_react.useCallback)(() => {
757
+ const currentActiveChannelId = activeChannelIdRef.current;
758
+ if (!currentActiveChannelId || !wsRef.current) return;
759
+ wsRef.current.send(JSON.stringify({ type: "typing.start", payload: { channel_id: currentActiveChannelId } }));
760
+ if (typingTimeout.current) clearTimeout(typingTimeout.current);
761
+ typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
762
+ }, [stopTyping]);
763
+ const createDMWithUser = (0, import_react.useCallback)(async (userId) => {
764
+ try {
765
+ const channel = await fetchFromComms("/channels/dm", {
766
+ method: "POST",
767
+ body: JSON.stringify({ user_id: userId })
768
+ });
769
+ await refreshChannels();
770
+ return channel.id;
771
+ } catch (error) {
772
+ console.error("[AegisChat] Failed to create DM:", error);
773
+ return null;
774
+ }
775
+ }, [fetchFromComms, refreshChannels]);
776
+ const retryMessage = (0, import_react.useCallback)(async (tempId) => {
777
+ const failedMessage = messages.find((m) => m.tempId === tempId && m.status === "failed");
778
+ const currentActiveChannelId = activeChannelIdRef.current;
779
+ if (!failedMessage || !currentActiveChannelId) return;
780
+ setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m));
781
+ try {
782
+ await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
783
+ method: "POST",
784
+ body: JSON.stringify({ content: failedMessage.content, type: failedMessage.type, metadata: failedMessage.metadata })
785
+ });
786
+ } catch (error) {
787
+ console.error("[AegisChat] Failed to retry message:", error);
788
+ setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
789
+ }
790
+ }, [messages, fetchFromComms]);
791
+ const deleteFailedMessage = (0, import_react.useCallback)((tempId) => {
792
+ setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
793
+ }, []);
794
+ (0, import_react.useEffect)(() => {
795
+ connect();
796
+ }, []);
797
+ (0, import_react.useEffect)(() => {
798
+ if (session && !isConnected && !isConnecting && autoConnect) {
799
+ connectWebSocket();
800
+ }
801
+ }, [session, isConnected, isConnecting, autoConnect, connectWebSocket]);
802
+ (0, import_react.useEffect)(() => {
803
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
804
+ wsRef.current.onmessage = (event) => {
805
+ try {
806
+ const data = JSON.parse(event.data);
807
+ handleWebSocketMessage(data);
808
+ } catch (error) {
809
+ console.error("[AegisChat] Failed to parse WebSocket message:", error);
810
+ }
811
+ };
812
+ }
813
+ }, [handleWebSocketMessage]);
814
+ (0, import_react.useEffect)(() => {
815
+ if (isConnected && channels.length === 0) {
816
+ refreshChannels();
817
+ }
818
+ }, [isConnected, channels.length, refreshChannels]);
819
+ (0, import_react.useEffect)(() => {
820
+ const storedActiveChannel = getActiveChannelId();
821
+ if (storedActiveChannel && !activeChannelId) {
822
+ selectChannel(storedActiveChannel);
823
+ }
824
+ }, [getActiveChannelId, activeChannelId, selectChannel]);
825
+ (0, import_react.useEffect)(() => {
826
+ return () => {
827
+ clearTimers();
828
+ if (typingTimeout.current) clearTimeout(typingTimeout.current);
829
+ if (wsRef.current) {
830
+ isManualDisconnect.current = true;
831
+ wsRef.current.close();
832
+ wsRef.current = null;
833
+ }
834
+ };
835
+ }, [clearTimers]);
836
+ return {
837
+ session,
838
+ isConnected,
839
+ isConnecting,
840
+ channels,
841
+ messages,
842
+ activeChannelId,
843
+ typingUsers: activeChannelId ? typingUsers[activeChannelId] || [] : [],
844
+ isLoadingChannels,
845
+ isLoadingMessages,
846
+ hasMoreMessages,
847
+ uploadProgress,
848
+ connect,
849
+ disconnect,
850
+ selectChannel,
851
+ sendMessage,
852
+ sendMessageWithFiles,
853
+ uploadFile,
854
+ loadMoreMessages,
855
+ startTyping,
856
+ stopTyping,
857
+ refreshChannels,
858
+ createDMWithUser,
859
+ retryMessage,
860
+ deleteFailedMessage,
861
+ markAsRead
862
+ };
863
+ }
864
+
865
+ // src/hooks/useAutoRead.ts
866
+ var import_react2 = require("react");
867
+ var SESSION_STORAGE_KEY2 = "@aegischat/activeChannel";
868
+ function useAutoRead(options = {}) {
869
+ const [isFocused, setIsFocused] = (0, import_react2.useState)(false);
870
+ (0, import_react2.useEffect)(() => {
871
+ setIsFocused(typeof document !== "undefined" && document.hasFocus());
872
+ const handleFocus = () => setIsFocused(true);
873
+ const handleBlur = () => setIsFocused(false);
874
+ window.addEventListener("focus", handleFocus);
875
+ window.addEventListener("blur", handleBlur);
876
+ return () => {
877
+ window.removeEventListener("focus", handleFocus);
878
+ window.removeEventListener("blur", handleBlur);
879
+ };
880
+ }, []);
881
+ (0, import_react2.useEffect)(() => {
882
+ const handleVisibilityChange = () => {
883
+ if (!document.hidden) {
884
+ const activeChannelId = sessionStorage.getItem(SESSION_STORAGE_KEY2);
885
+ if (activeChannelId) {
886
+ markAsRead(activeChannelId);
887
+ }
888
+ }
889
+ };
890
+ document.addEventListener("visibilitychange", handleVisibilityChange);
891
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
892
+ }, []);
893
+ const markAsRead = (0, import_react2.useCallback)(async (channelId) => {
894
+ if (!isFocused) return;
895
+ try {
896
+ await channelsApi.markAsRead(channelId);
897
+ options.onMarkAsRead?.(channelId);
898
+ } catch (error) {
899
+ console.error("[AegisChat] useAutoRead: Failed to mark as read:", error);
900
+ }
901
+ }, [isFocused, options.onMarkAsRead]);
902
+ const markAllAsRead = (0, import_react2.useCallback)(async () => {
903
+ if (!isFocused) return;
904
+ try {
905
+ const response = await channelsApi.list({});
906
+ const channels = response.data.channels || [];
907
+ await Promise.all(
908
+ channels.filter((ch) => ch.unread_count > 0).map((ch) => channelsApi.markAsRead(ch.id))
909
+ );
910
+ } catch (error) {
911
+ console.error("[AegisChat] useAutoRead: Failed to mark all as read:", error);
912
+ }
913
+ }, [isFocused]);
914
+ return { markAsRead, markAllAsRead, isFocused };
915
+ }
916
+
917
+ // src/hooks/useChannels.ts
918
+ var import_react3 = require("react");
919
+ function useChannels(options = {}) {
920
+ const { type, limit = 20, autoFetch = true, onChannelCreated, onError } = options;
921
+ const [channels, setChannels] = (0, import_react3.useState)([]);
922
+ const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
923
+ const [error, setError] = (0, import_react3.useState)(null);
924
+ const abortControllerRef = (0, import_react3.useRef)(null);
925
+ const fetchChannels = (0, import_react3.useCallback)(async (signal) => {
926
+ setIsLoading(true);
927
+ setError(null);
928
+ try {
929
+ const response = await channelsApi.list({ type, limit }, signal);
930
+ if (signal?.aborted) return;
931
+ setChannels(response.data.channels);
932
+ } catch (err) {
933
+ if (err instanceof Error && err.name === "AbortError") return;
934
+ const error2 = err instanceof Error ? err : new Error("Failed to fetch channels");
935
+ setError(error2);
936
+ onError?.(error2);
937
+ } finally {
938
+ if (!signal?.aborted) setIsLoading(false);
939
+ }
940
+ }, [type, limit, onError]);
941
+ const refetch = (0, import_react3.useCallback)(async () => {
942
+ if (abortControllerRef.current) abortControllerRef.current.abort();
943
+ const controller = new AbortController();
944
+ abortControllerRef.current = controller;
945
+ await fetchChannels(controller.signal);
946
+ }, [fetchChannels]);
947
+ const getOrCreateDM = (0, import_react3.useCallback)(async (userId) => {
948
+ try {
949
+ const response = await channelsApi.getOrCreateDM(userId);
950
+ if (abortControllerRef.current) abortControllerRef.current.abort();
951
+ const controller = new AbortController();
952
+ abortControllerRef.current = controller;
953
+ fetchChannels(controller.signal);
954
+ onChannelCreated?.(response.data);
955
+ return response.data;
956
+ } catch (err) {
957
+ const error2 = err instanceof Error ? err : new Error("Failed to create DM");
958
+ onError?.(error2);
959
+ throw error2;
960
+ }
961
+ }, [fetchChannels, onChannelCreated, onError]);
962
+ const markAsRead = (0, import_react3.useCallback)(async (channelId) => {
963
+ try {
964
+ await channelsApi.markAsRead(channelId);
965
+ setChannels((prev) => prev.map((ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch));
966
+ } catch (err) {
967
+ const error2 = err instanceof Error ? err : new Error("Failed to mark as read");
968
+ onError?.(error2);
969
+ throw error2;
970
+ }
971
+ }, [onError]);
972
+ (0, import_react3.useEffect)(() => {
973
+ if (autoFetch) {
974
+ if (abortControllerRef.current) abortControllerRef.current.abort();
975
+ const controller = new AbortController();
976
+ abortControllerRef.current = controller;
977
+ fetchChannels(controller.signal);
978
+ return () => controller.abort();
979
+ }
980
+ }, [autoFetch, fetchChannels]);
981
+ return { channels, isLoading, error, refetch, getOrCreateDM, markAsRead };
982
+ }
983
+
984
+ // src/hooks/useMessages.ts
985
+ var import_react4 = require("react");
986
+ function useMessages(_options) {
987
+ const [messages, setMessages] = (0, import_react4.useState)([]);
988
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
989
+ const [hasMore, setHasMore] = (0, import_react4.useState)(true);
990
+ const sendMessage = (0, import_react4.useCallback)(async (params) => {
991
+ }, []);
992
+ const loadMore = (0, import_react4.useCallback)(async () => {
993
+ }, []);
994
+ return { messages, isLoading, hasMore, sendMessage, loadMore };
995
+ }
996
+
997
+ // src/hooks/useTypingIndicator.ts
998
+ var import_react5 = require("react");
999
+ function useTypingIndicator(_options) {
1000
+ const [typingUsers, setTypingUsers] = (0, import_react5.useState)([]);
1001
+ const startTyping = (0, import_react5.useCallback)(() => {
1002
+ }, []);
1003
+ const stopTyping = (0, import_react5.useCallback)(() => {
1004
+ }, []);
1005
+ return { typingUsers, startTyping, stopTyping };
1006
+ }
1007
+
1008
+ // src/hooks/useReactions.ts
1009
+ var import_react6 = require("react");
1010
+ function useReactions(_options) {
1011
+ const [reactions, setReactions] = (0, import_react6.useState)([]);
1012
+ const addReaction = async (_emoji) => {
1013
+ };
1014
+ const removeReaction = async (_emoji) => {
1015
+ };
1016
+ return { reactions, addReaction, removeReaction };
1017
+ }
1018
+
1019
+ // src/hooks/useFileUpload.ts
1020
+ var import_react7 = require("react");
1021
+ function useFileUpload(_options) {
1022
+ const [uploadProgress, setUploadProgress] = (0, import_react7.useState)([]);
1023
+ const upload = async (_file) => null;
1024
+ return { uploadProgress, upload };
1025
+ }
1026
+
1027
+ // src/hooks/useMentions.ts
1028
+ function useMentions(_options = {}) {
1029
+ const parseMentions = (content) => {
1030
+ const mentionRegex = /@\[([^\]]+)\]\(([^)]+)\)/g;
1031
+ const mentions = [];
1032
+ let match;
1033
+ while ((match = mentionRegex.exec(content)) !== null) {
1034
+ mentions.push(match[2]);
1035
+ }
1036
+ return mentions;
1037
+ };
1038
+ const highlightMentions = (content) => {
1039
+ return content.replace(/@\[([^\]]+)\]\(([^)]+)\)/g, '<span class="mention">@$1</span>');
1040
+ };
1041
+ const isUserMentioned = (content, userId) => {
1042
+ const mentions = parseMentions(content);
1043
+ return mentions.includes(userId);
1044
+ };
1045
+ return { parseMentions, highlightMentions, isUserMentioned };
1046
+ }
1047
+ // Annotate the CommonJS export names for ESM import in node:
1048
+ 0 && (module.exports = {
1049
+ channelsApi,
1050
+ chatApi,
1051
+ configureApiClient,
1052
+ filesApi,
1053
+ messagesApi,
1054
+ reactionsApi,
1055
+ useAutoRead,
1056
+ useChannels,
1057
+ useChat,
1058
+ useFileUpload,
1059
+ useMentions,
1060
+ useMessages,
1061
+ useReactions,
1062
+ useTypingIndicator,
1063
+ usersApi
1064
+ });
1065
+ //# sourceMappingURL=index.js.map