@thrillee/aegischat 0.1.5 → 0.1.7
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 +590 -352
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -352
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useChat.ts +721 -389
- package/src/services/api.ts +22 -22
package/dist/index.js
CHANGED
|
@@ -75,7 +75,7 @@ var chatApi = {
|
|
|
75
75
|
* Connect to chat session
|
|
76
76
|
*/
|
|
77
77
|
async connect(params, signal) {
|
|
78
|
-
return fetchWithAuth("/
|
|
78
|
+
return fetchWithAuth("/chat/connect", {
|
|
79
79
|
method: "POST",
|
|
80
80
|
body: JSON.stringify(params),
|
|
81
81
|
signal
|
|
@@ -85,7 +85,7 @@ var chatApi = {
|
|
|
85
85
|
* Refresh access token
|
|
86
86
|
*/
|
|
87
87
|
async refreshToken(refreshToken, signal) {
|
|
88
|
-
return fetchWithAuth("/
|
|
88
|
+
return fetchWithAuth("/chat/refresh", {
|
|
89
89
|
method: "POST",
|
|
90
90
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
91
91
|
signal
|
|
@@ -101,19 +101,19 @@ var channelsApi = {
|
|
|
101
101
|
if (options.type) params.append("type", options.type);
|
|
102
102
|
if (options.limit) params.append("limit", String(options.limit));
|
|
103
103
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
104
|
-
return fetchWithAuth(`/
|
|
104
|
+
return fetchWithAuth(`/channels${query}`, { signal });
|
|
105
105
|
},
|
|
106
106
|
/**
|
|
107
107
|
* Get channel by ID
|
|
108
108
|
*/
|
|
109
109
|
async get(channelId, signal) {
|
|
110
|
-
return fetchWithAuth(`/
|
|
110
|
+
return fetchWithAuth(`/channels/${channelId}`, { signal });
|
|
111
111
|
},
|
|
112
112
|
/**
|
|
113
113
|
* Get or create DM channel
|
|
114
114
|
*/
|
|
115
115
|
async getOrCreateDM(userId, signal) {
|
|
116
|
-
return fetchWithAuth("/
|
|
116
|
+
return fetchWithAuth("/channels/dm", {
|
|
117
117
|
method: "POST",
|
|
118
118
|
body: JSON.stringify({ user_id: userId }),
|
|
119
119
|
signal
|
|
@@ -123,7 +123,7 @@ var channelsApi = {
|
|
|
123
123
|
* Create channel
|
|
124
124
|
*/
|
|
125
125
|
async create(data, signal) {
|
|
126
|
-
return fetchWithAuth("/
|
|
126
|
+
return fetchWithAuth("/channels", {
|
|
127
127
|
method: "POST",
|
|
128
128
|
body: JSON.stringify(data),
|
|
129
129
|
signal
|
|
@@ -133,7 +133,7 @@ var channelsApi = {
|
|
|
133
133
|
* Mark channel as read
|
|
134
134
|
*/
|
|
135
135
|
async markAsRead(channelId, signal) {
|
|
136
|
-
return fetchWithAuth(`/
|
|
136
|
+
return fetchWithAuth(`/channels/${channelId}/read`, {
|
|
137
137
|
method: "POST",
|
|
138
138
|
signal
|
|
139
139
|
});
|
|
@@ -142,13 +142,13 @@ var channelsApi = {
|
|
|
142
142
|
* Get channel members
|
|
143
143
|
*/
|
|
144
144
|
async getMembers(channelId, signal) {
|
|
145
|
-
return fetchWithAuth(`/
|
|
145
|
+
return fetchWithAuth(`/channels/${channelId}/members`, { signal });
|
|
146
146
|
},
|
|
147
147
|
/**
|
|
148
148
|
* Update channel
|
|
149
149
|
*/
|
|
150
150
|
async update(channelId, data, signal) {
|
|
151
|
-
return fetchWithAuth(`/
|
|
151
|
+
return fetchWithAuth(`/channels/${channelId}`, {
|
|
152
152
|
method: "PATCH",
|
|
153
153
|
body: JSON.stringify(data),
|
|
154
154
|
signal
|
|
@@ -164,13 +164,13 @@ var messagesApi = {
|
|
|
164
164
|
if (options.limit) params.append("limit", String(options.limit));
|
|
165
165
|
if (options.before) params.append("before", options.before);
|
|
166
166
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
167
|
-
return fetchWithAuth(`/
|
|
167
|
+
return fetchWithAuth(`/channels/${channelId}/messages${query}`, { signal });
|
|
168
168
|
},
|
|
169
169
|
/**
|
|
170
170
|
* Send a message
|
|
171
171
|
*/
|
|
172
172
|
async send(channelId, data, signal) {
|
|
173
|
-
return fetchWithAuth(`/
|
|
173
|
+
return fetchWithAuth(`/channels/${channelId}/messages`, {
|
|
174
174
|
method: "POST",
|
|
175
175
|
body: JSON.stringify(data),
|
|
176
176
|
signal
|
|
@@ -180,7 +180,7 @@ var messagesApi = {
|
|
|
180
180
|
* Update a message
|
|
181
181
|
*/
|
|
182
182
|
async update(channelId, messageId, data, signal) {
|
|
183
|
-
return fetchWithAuth(`/
|
|
183
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}`, {
|
|
184
184
|
method: "PATCH",
|
|
185
185
|
body: JSON.stringify(data),
|
|
186
186
|
signal
|
|
@@ -190,7 +190,7 @@ var messagesApi = {
|
|
|
190
190
|
* Delete a message
|
|
191
191
|
*/
|
|
192
192
|
async delete(channelId, messageId, signal) {
|
|
193
|
-
return fetchWithAuth(`/
|
|
193
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}`, {
|
|
194
194
|
method: "DELETE",
|
|
195
195
|
signal
|
|
196
196
|
});
|
|
@@ -199,7 +199,7 @@ var messagesApi = {
|
|
|
199
199
|
* Mark messages as delivered
|
|
200
200
|
*/
|
|
201
201
|
async markDelivered(channelId, signal) {
|
|
202
|
-
return fetchWithAuth(`/
|
|
202
|
+
return fetchWithAuth(`/channels/${channelId}/messages/delivered`, {
|
|
203
203
|
method: "POST",
|
|
204
204
|
signal
|
|
205
205
|
});
|
|
@@ -208,7 +208,7 @@ var messagesApi = {
|
|
|
208
208
|
* Mark messages as read
|
|
209
209
|
*/
|
|
210
210
|
async markRead(channelId, signal) {
|
|
211
|
-
return fetchWithAuth(`/
|
|
211
|
+
return fetchWithAuth(`/channels/${channelId}/messages/read`, {
|
|
212
212
|
method: "POST",
|
|
213
213
|
signal
|
|
214
214
|
});
|
|
@@ -219,7 +219,7 @@ var reactionsApi = {
|
|
|
219
219
|
* Add reaction to a message
|
|
220
220
|
*/
|
|
221
221
|
async add(channelId, messageId, emoji, signal) {
|
|
222
|
-
return fetchWithAuth(`/
|
|
222
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}/reactions`, {
|
|
223
223
|
method: "POST",
|
|
224
224
|
body: JSON.stringify({ emoji }),
|
|
225
225
|
signal
|
|
@@ -230,7 +230,7 @@ var reactionsApi = {
|
|
|
230
230
|
*/
|
|
231
231
|
async remove(channelId, messageId, emoji, signal) {
|
|
232
232
|
return fetchWithAuth(
|
|
233
|
-
`/
|
|
233
|
+
`/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`,
|
|
234
234
|
{ method: "DELETE", signal }
|
|
235
235
|
);
|
|
236
236
|
}
|
|
@@ -240,7 +240,7 @@ var filesApi = {
|
|
|
240
240
|
* Get upload URL
|
|
241
241
|
*/
|
|
242
242
|
async getUploadUrl(data, signal) {
|
|
243
|
-
return fetchWithAuth("/
|
|
243
|
+
return fetchWithAuth("/files/upload-url", {
|
|
244
244
|
method: "POST",
|
|
245
245
|
body: JSON.stringify(data),
|
|
246
246
|
signal
|
|
@@ -250,7 +250,7 @@ var filesApi = {
|
|
|
250
250
|
* Confirm file upload
|
|
251
251
|
*/
|
|
252
252
|
async confirm(fileId, signal) {
|
|
253
|
-
return fetchWithAuth("/
|
|
253
|
+
return fetchWithAuth("/files", {
|
|
254
254
|
method: "POST",
|
|
255
255
|
body: JSON.stringify({ file_id: fileId }),
|
|
256
256
|
signal
|
|
@@ -260,7 +260,7 @@ var filesApi = {
|
|
|
260
260
|
* Get download URL
|
|
261
261
|
*/
|
|
262
262
|
async getDownloadUrl(fileId, signal) {
|
|
263
|
-
return fetchWithAuth(`/
|
|
263
|
+
return fetchWithAuth(`/files/${fileId}/download`, { signal });
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
266
|
var usersApi = {
|
|
@@ -268,13 +268,13 @@ var usersApi = {
|
|
|
268
268
|
* Search users
|
|
269
269
|
*/
|
|
270
270
|
async search(query, signal) {
|
|
271
|
-
return fetchWithAuth(`/
|
|
271
|
+
return fetchWithAuth(`/users/search?q=${encodeURIComponent(query)}`, { signal });
|
|
272
272
|
},
|
|
273
273
|
/**
|
|
274
274
|
* Get user by ID
|
|
275
275
|
*/
|
|
276
276
|
async get(userId, signal) {
|
|
277
|
-
return fetchWithAuth(`/
|
|
277
|
+
return fetchWithAuth(`/users/${userId}`, { signal });
|
|
278
278
|
}
|
|
279
279
|
};
|
|
280
280
|
|
|
@@ -285,15 +285,28 @@ var MAX_RECONNECT_ATTEMPTS = 5;
|
|
|
285
285
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
286
286
|
var PING_INTERVAL = 3e4;
|
|
287
287
|
var SESSION_STORAGE_KEY = "@aegischat/activeChannel";
|
|
288
|
-
function useChat(options) {
|
|
289
|
-
const {
|
|
290
|
-
|
|
288
|
+
function useChat(options = {}) {
|
|
289
|
+
const {
|
|
290
|
+
config,
|
|
291
|
+
role,
|
|
292
|
+
clientId,
|
|
293
|
+
initialSession,
|
|
294
|
+
autoConnect = true,
|
|
295
|
+
onMessage,
|
|
296
|
+
onTyping,
|
|
297
|
+
onConnectionChange
|
|
298
|
+
} = options;
|
|
299
|
+
const [session, setSession] = (0, import_react.useState)(null);
|
|
291
300
|
const [isConnected, setIsConnected] = (0, import_react.useState)(false);
|
|
292
301
|
const [isConnecting, setIsConnecting] = (0, import_react.useState)(false);
|
|
293
|
-
const [activeChannelId, setActiveChannelIdState] = (0, import_react.useState)(
|
|
302
|
+
const [activeChannelId, setActiveChannelIdState] = (0, import_react.useState)(
|
|
303
|
+
null
|
|
304
|
+
);
|
|
294
305
|
const [channels, setChannels] = (0, import_react.useState)([]);
|
|
295
306
|
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
296
|
-
const [typingUsers, setTypingUsers] = (0, import_react.useState)(
|
|
307
|
+
const [typingUsers, setTypingUsers] = (0, import_react.useState)(
|
|
308
|
+
{}
|
|
309
|
+
);
|
|
297
310
|
const [isLoadingChannels, setIsLoadingChannels] = (0, import_react.useState)(false);
|
|
298
311
|
const [isLoadingMessages, setIsLoadingMessages] = (0, import_react.useState)(false);
|
|
299
312
|
const [hasMoreMessages, setHasMoreMessages] = (0, import_react.useState)(true);
|
|
@@ -306,23 +319,19 @@ function useChat(options) {
|
|
|
306
319
|
const isManualDisconnect = (0, import_react.useRef)(false);
|
|
307
320
|
const oldestMessageId = (0, import_react.useRef)(null);
|
|
308
321
|
const activeChannelIdRef = (0, import_react.useRef)(null);
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
(0, import_react.useEffect)(() => {
|
|
318
|
-
configRef.current = config;
|
|
319
|
-
}, [config]);
|
|
322
|
+
const sessionRef = (0, import_react.useRef)(null);
|
|
323
|
+
const roleRef = (0, import_react.useRef)(void 0);
|
|
324
|
+
const clientIdRef = (0, import_react.useRef)(void 0);
|
|
325
|
+
const autoConnectRef = (0, import_react.useRef)(true);
|
|
326
|
+
const onMessageRef = (0, import_react.useRef)(void 0);
|
|
327
|
+
const onTypingRef = (0, import_react.useRef)(void 0);
|
|
328
|
+
const onConnectionChangeRef = (0, import_react.useRef)(void 0);
|
|
320
329
|
(0, import_react.useEffect)(() => {
|
|
321
330
|
activeChannelIdRef.current = activeChannelId;
|
|
322
331
|
}, [activeChannelId]);
|
|
323
332
|
(0, import_react.useEffect)(() => {
|
|
324
|
-
|
|
325
|
-
}, [
|
|
333
|
+
activeChannelIdRef.current = activeChannelId;
|
|
334
|
+
}, [activeChannelId]);
|
|
326
335
|
const getActiveChannelId = (0, import_react.useCallback)(() => {
|
|
327
336
|
if (typeof window === "undefined") return null;
|
|
328
337
|
return sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -337,26 +346,29 @@ function useChat(options) {
|
|
|
337
346
|
}
|
|
338
347
|
}
|
|
339
348
|
}, []);
|
|
340
|
-
const fetchFromComms = (0, import_react.useCallback)(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const response = await fetch(`${currentSession.api_url}${path}`, {
|
|
346
|
-
...fetchOptions,
|
|
347
|
-
headers: {
|
|
348
|
-
"Content-Type": "application/json",
|
|
349
|
-
Authorization: `Bearer ${currentSession.access_token}`,
|
|
350
|
-
...fetchOptions.headers
|
|
349
|
+
const fetchFromComms = (0, import_react.useCallback)(
|
|
350
|
+
async (path, fetchOptions = {}) => {
|
|
351
|
+
const currentSession = sessionRef.current;
|
|
352
|
+
if (!currentSession) {
|
|
353
|
+
throw new Error("Chat session not initialized");
|
|
351
354
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
const response = await fetch(`${currentSession.api_url}${path}`, {
|
|
356
|
+
...fetchOptions,
|
|
357
|
+
headers: {
|
|
358
|
+
"Content-Type": "application/json",
|
|
359
|
+
Authorization: `Bearer ${currentSession.access_token}`,
|
|
360
|
+
...fetchOptions.headers
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
const error = await response.json().catch(() => ({}));
|
|
365
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
366
|
+
}
|
|
367
|
+
const data = await response.json();
|
|
368
|
+
return data.data || data;
|
|
369
|
+
},
|
|
370
|
+
[]
|
|
371
|
+
);
|
|
360
372
|
const clearTimers = (0, import_react.useCallback)(() => {
|
|
361
373
|
if (reconnectTimeout.current) {
|
|
362
374
|
clearTimeout(reconnectTimeout.current);
|
|
@@ -367,111 +379,141 @@ function useChat(options) {
|
|
|
367
379
|
pingInterval.current = null;
|
|
368
380
|
}
|
|
369
381
|
}, []);
|
|
370
|
-
const handleWebSocketMessage = (0, import_react.useCallback)(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
382
|
+
const handleWebSocketMessage = (0, import_react.useCallback)(
|
|
383
|
+
(data) => {
|
|
384
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
385
|
+
console.log("[AegisChat] WebSocket message received:", data.type, data);
|
|
386
|
+
switch (data.type) {
|
|
387
|
+
case "message.new": {
|
|
388
|
+
const newMessage = data.payload;
|
|
389
|
+
if (newMessage.channel_id === currentActiveChannelId) {
|
|
390
|
+
setMessages((prev) => {
|
|
391
|
+
const existingIndex = prev.findIndex(
|
|
392
|
+
(m) => m.tempId && m.content === newMessage.content && m.status === "sending"
|
|
393
|
+
);
|
|
394
|
+
if (existingIndex !== -1) {
|
|
395
|
+
const updated = [...prev];
|
|
396
|
+
updated[existingIndex] = { ...newMessage, status: "sent" };
|
|
397
|
+
return updated;
|
|
398
|
+
}
|
|
399
|
+
if (prev.some((m) => m.id === newMessage.id)) return prev;
|
|
400
|
+
return [...prev, { ...newMessage, status: "delivered" }];
|
|
401
|
+
});
|
|
402
|
+
onMessageRef.current?.(newMessage);
|
|
403
|
+
}
|
|
404
|
+
setChannels((prev) => {
|
|
405
|
+
const updated = prev.map(
|
|
406
|
+
(ch) => ch.id === newMessage.channel_id ? {
|
|
407
|
+
...ch,
|
|
408
|
+
last_message: {
|
|
409
|
+
id: newMessage.id,
|
|
410
|
+
content: newMessage.content,
|
|
411
|
+
created_at: newMessage.created_at,
|
|
412
|
+
sender: {
|
|
413
|
+
id: newMessage.sender_id,
|
|
414
|
+
display_name: "Unknown",
|
|
415
|
+
status: "online"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
|
|
419
|
+
} : ch
|
|
380
420
|
);
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
if (prev.some((m) => m.id === newMessage.id)) return prev;
|
|
387
|
-
return [...prev, { ...newMessage, status: "delivered" }];
|
|
421
|
+
return updated.sort((a, b) => {
|
|
422
|
+
const timeA = a.last_message?.created_at || "";
|
|
423
|
+
const timeB = b.last_message?.created_at || "";
|
|
424
|
+
return timeB.localeCompare(timeA);
|
|
425
|
+
});
|
|
388
426
|
});
|
|
389
|
-
|
|
427
|
+
break;
|
|
390
428
|
}
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
(ch) => ch.id === newMessage.channel_id ? {
|
|
394
|
-
...ch,
|
|
395
|
-
last_message: {
|
|
396
|
-
id: newMessage.id,
|
|
397
|
-
content: newMessage.content,
|
|
398
|
-
created_at: newMessage.created_at,
|
|
399
|
-
sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
|
|
400
|
-
},
|
|
401
|
-
unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
|
|
402
|
-
} : ch
|
|
403
|
-
);
|
|
404
|
-
return updated.sort((a, b) => {
|
|
405
|
-
const timeA = a.last_message?.created_at || "";
|
|
406
|
-
const timeB = b.last_message?.created_at || "";
|
|
407
|
-
return timeB.localeCompare(timeA);
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
case "message.updated": {
|
|
413
|
-
const updatedMessage = data.payload;
|
|
414
|
-
setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
case "message.deleted": {
|
|
418
|
-
const { message_id } = data.payload;
|
|
419
|
-
setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
case "message.delivered":
|
|
423
|
-
case "message.read": {
|
|
424
|
-
const { message_id, channel_id, status } = data.payload;
|
|
425
|
-
if (channel_id === currentActiveChannelId) {
|
|
429
|
+
case "message.updated": {
|
|
430
|
+
const updatedMessage = data.payload;
|
|
426
431
|
setMessages(
|
|
427
|
-
(prev) => prev.map((m) => m.id ===
|
|
432
|
+
(prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
|
|
428
433
|
);
|
|
434
|
+
break;
|
|
429
435
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
case "message.delivered.batch":
|
|
433
|
-
case "message.read.batch": {
|
|
434
|
-
const { channel_id } = data.payload;
|
|
435
|
-
if (channel_id === currentActiveChannelId) {
|
|
436
|
+
case "message.deleted": {
|
|
437
|
+
const { message_id } = data.payload;
|
|
436
438
|
setMessages(
|
|
437
|
-
(prev) => prev.map(
|
|
439
|
+
(prev) => prev.map(
|
|
440
|
+
(m) => m.id === message_id ? { ...m, deleted: true } : m
|
|
441
|
+
)
|
|
438
442
|
);
|
|
443
|
+
break;
|
|
439
444
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
445
|
+
case "message.delivered":
|
|
446
|
+
case "message.read": {
|
|
447
|
+
const { message_id, channel_id, status } = data.payload;
|
|
448
|
+
if (channel_id === currentActiveChannelId) {
|
|
449
|
+
setMessages(
|
|
450
|
+
(prev) => prev.map(
|
|
451
|
+
(m) => m.id === message_id ? {
|
|
452
|
+
...m,
|
|
453
|
+
status: status || "delivered"
|
|
454
|
+
} : m
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "message.delivered.batch":
|
|
461
|
+
case "message.read.batch": {
|
|
462
|
+
const { channel_id } = data.payload;
|
|
463
|
+
if (channel_id === currentActiveChannelId) {
|
|
464
|
+
setMessages(
|
|
465
|
+
(prev) => prev.map(
|
|
466
|
+
(m) => m.status === "sent" || m.status === "delivered" ? {
|
|
467
|
+
...m,
|
|
468
|
+
status: data.type === "message.delivered.batch" ? "delivered" : "read"
|
|
469
|
+
} : m
|
|
470
|
+
)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case "typing.start": {
|
|
476
|
+
const { channel_id, user } = data.payload;
|
|
477
|
+
const typingUser = {
|
|
478
|
+
id: user.id,
|
|
479
|
+
displayName: user.display_name,
|
|
480
|
+
avatarUrl: user.avatar_url,
|
|
481
|
+
startedAt: Date.now()
|
|
482
|
+
};
|
|
483
|
+
setTypingUsers((prev) => ({
|
|
484
|
+
...prev,
|
|
485
|
+
[channel_id]: [
|
|
486
|
+
...(prev[channel_id] || []).filter((u) => u.id !== user.id),
|
|
487
|
+
typingUser
|
|
488
|
+
]
|
|
489
|
+
}));
|
|
490
|
+
onTypingRef.current?.(channel_id, typingUser);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case "typing.stop": {
|
|
494
|
+
const { channel_id, user_id } = data.payload;
|
|
495
|
+
setTypingUsers((prev) => ({
|
|
496
|
+
...prev,
|
|
497
|
+
[channel_id]: (prev[channel_id] || []).filter(
|
|
498
|
+
(u) => u.id !== user_id
|
|
499
|
+
)
|
|
500
|
+
}));
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "pong":
|
|
504
|
+
break;
|
|
505
|
+
default:
|
|
506
|
+
console.log("[AegisChat] Unhandled message type:", data.type);
|
|
464
507
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
console.log("[AegisChat] Unhandled message type:", data.type);
|
|
469
|
-
}
|
|
470
|
-
}, [onMessage, onTyping]);
|
|
508
|
+
},
|
|
509
|
+
[]
|
|
510
|
+
);
|
|
471
511
|
const connectWebSocket = (0, import_react.useCallback)(() => {
|
|
472
512
|
const currentSession = sessionRef.current;
|
|
473
513
|
if (!currentSession?.websocket_url || !currentSession?.access_token) {
|
|
474
|
-
console.warn(
|
|
514
|
+
console.warn(
|
|
515
|
+
"[AegisChat] Cannot connect WebSocket - missing session or token"
|
|
516
|
+
);
|
|
475
517
|
return;
|
|
476
518
|
}
|
|
477
519
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
@@ -488,14 +530,19 @@ function useChat(options) {
|
|
|
488
530
|
setIsConnected(true);
|
|
489
531
|
setIsConnecting(false);
|
|
490
532
|
reconnectAttempts.current = 0;
|
|
491
|
-
|
|
533
|
+
onConnectionChangeRef.current?.(true);
|
|
492
534
|
pingInterval.current = setInterval(() => {
|
|
493
535
|
if (ws.readyState === WebSocket.OPEN) {
|
|
494
536
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
495
537
|
}
|
|
496
538
|
}, PING_INTERVAL);
|
|
497
539
|
if (activeChannelIdRef.current) {
|
|
498
|
-
ws.send(
|
|
540
|
+
ws.send(
|
|
541
|
+
JSON.stringify({
|
|
542
|
+
type: "channel.join",
|
|
543
|
+
payload: { channel_id: activeChannelIdRef.current }
|
|
544
|
+
})
|
|
545
|
+
);
|
|
499
546
|
}
|
|
500
547
|
};
|
|
501
548
|
ws.onmessage = (event) => {
|
|
@@ -511,9 +558,12 @@ function useChat(options) {
|
|
|
511
558
|
setIsConnected(false);
|
|
512
559
|
setIsConnecting(false);
|
|
513
560
|
clearTimers();
|
|
514
|
-
|
|
561
|
+
onConnectionChangeRef.current?.(false);
|
|
515
562
|
if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
|
|
516
|
-
const delay = Math.min(
|
|
563
|
+
const delay = Math.min(
|
|
564
|
+
RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current),
|
|
565
|
+
MAX_RECONNECT_DELAY
|
|
566
|
+
);
|
|
517
567
|
console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
|
|
518
568
|
reconnectTimeout.current = setTimeout(() => {
|
|
519
569
|
reconnectAttempts.current++;
|
|
@@ -525,14 +575,18 @@ function useChat(options) {
|
|
|
525
575
|
console.error("[AegisChat] WebSocket error:", error);
|
|
526
576
|
};
|
|
527
577
|
wsRef.current = ws;
|
|
528
|
-
}, [clearTimers, handleWebSocketMessage
|
|
578
|
+
}, [clearTimers, handleWebSocketMessage]);
|
|
529
579
|
const connect = (0, import_react.useCallback)(async () => {
|
|
530
580
|
console.log("[AegisChat] connect() called");
|
|
531
|
-
const targetSession = sessionRef.current
|
|
581
|
+
const targetSession = sessionRef.current;
|
|
532
582
|
if (!targetSession) {
|
|
533
583
|
console.log("[AegisChat] No session available, skipping connect");
|
|
534
584
|
return;
|
|
535
585
|
}
|
|
586
|
+
if (!autoConnectRef.current) {
|
|
587
|
+
console.log("[AegisChat] autoConnect is false, skipping connect");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
536
590
|
connectWebSocket();
|
|
537
591
|
}, [connectWebSocket]);
|
|
538
592
|
const disconnect = (0, import_react.useCallback)(() => {
|
|
@@ -560,48 +614,72 @@ function useChat(options) {
|
|
|
560
614
|
setIsLoadingChannels(false);
|
|
561
615
|
}
|
|
562
616
|
}, []);
|
|
563
|
-
const selectChannel = (0, import_react.useCallback)(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
|
|
617
|
+
const selectChannel = (0, import_react.useCallback)(
|
|
618
|
+
async (channelId) => {
|
|
619
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
620
|
+
setActiveChannelId(channelId);
|
|
621
|
+
setMessages([]);
|
|
622
|
+
setHasMoreMessages(true);
|
|
623
|
+
oldestMessageId.current = null;
|
|
624
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
625
|
+
if (currentActiveChannelId) {
|
|
626
|
+
wsRef.current.send(
|
|
627
|
+
JSON.stringify({
|
|
628
|
+
type: "channel.leave",
|
|
629
|
+
payload: { channel_id: currentActiveChannelId }
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
wsRef.current.send(
|
|
634
|
+
JSON.stringify({
|
|
635
|
+
type: "channel.join",
|
|
636
|
+
payload: { channel_id: channelId }
|
|
637
|
+
})
|
|
638
|
+
);
|
|
572
639
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
640
|
+
setIsLoadingMessages(true);
|
|
641
|
+
try {
|
|
642
|
+
const response = await fetchFromComms(
|
|
643
|
+
`/channels/${channelId}/messages?limit=50`
|
|
644
|
+
);
|
|
645
|
+
setMessages(response.messages || []);
|
|
646
|
+
setHasMoreMessages(response.has_more);
|
|
647
|
+
if (response.oldest_id) {
|
|
648
|
+
oldestMessageId.current = response.oldest_id;
|
|
649
|
+
}
|
|
650
|
+
await markAsRead(channelId);
|
|
651
|
+
setChannels(
|
|
652
|
+
(prev) => prev.map(
|
|
653
|
+
(ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.error("[AegisChat] Failed to load messages:", error);
|
|
658
|
+
setMessages([]);
|
|
659
|
+
} finally {
|
|
660
|
+
setIsLoadingMessages(false);
|
|
582
661
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
console.error("[AegisChat] Failed to mark as read:", error);
|
|
597
|
-
}
|
|
598
|
-
}, [fetchFromComms]);
|
|
662
|
+
},
|
|
663
|
+
[setActiveChannelId, fetchFromComms]
|
|
664
|
+
);
|
|
665
|
+
const markAsRead = (0, import_react.useCallback)(
|
|
666
|
+
async (channelId) => {
|
|
667
|
+
try {
|
|
668
|
+
await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error("[AegisChat] Failed to mark as read:", error);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
[fetchFromComms]
|
|
674
|
+
);
|
|
599
675
|
const loadMoreMessages = (0, import_react.useCallback)(async () => {
|
|
600
676
|
if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
|
|
601
677
|
setIsLoadingMessages(true);
|
|
602
678
|
try {
|
|
603
679
|
const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
|
|
604
|
-
const response = await fetchFromComms(
|
|
680
|
+
const response = await fetchFromComms(
|
|
681
|
+
`/channels/${activeChannelId}/messages${params}`
|
|
682
|
+
);
|
|
605
683
|
setMessages((prev) => [...response.messages || [], ...prev]);
|
|
606
684
|
setHasMoreMessages(response.has_more);
|
|
607
685
|
if (response.oldest_id) {
|
|
@@ -613,138 +691,234 @@ function useChat(options) {
|
|
|
613
691
|
setIsLoadingMessages(false);
|
|
614
692
|
}
|
|
615
693
|
}, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
|
|
616
|
-
const sendMessage = (0, import_react.useCallback)(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
});
|
|
659
|
-
} catch (error) {
|
|
660
|
-
console.error("[AegisChat] Failed to send message:", error);
|
|
661
|
-
setMessages(
|
|
662
|
-
(prev) => prev.map(
|
|
663
|
-
(m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
|
|
664
|
-
)
|
|
665
|
-
);
|
|
666
|
-
throw error;
|
|
667
|
-
}
|
|
668
|
-
}, [fetchFromComms]);
|
|
669
|
-
const uploadFile = (0, import_react.useCallback)(async (file) => {
|
|
670
|
-
const currentSession = sessionRef.current;
|
|
671
|
-
if (!currentSession) return null;
|
|
672
|
-
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
673
|
-
setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
|
|
674
|
-
try {
|
|
675
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
|
|
676
|
-
const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
|
|
677
|
-
method: "POST",
|
|
678
|
-
body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
|
|
679
|
-
});
|
|
680
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
|
|
681
|
-
const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
|
|
682
|
-
method: "PUT",
|
|
683
|
-
body: file,
|
|
684
|
-
headers: { "Content-Type": file.type || "application/octet-stream" }
|
|
685
|
-
});
|
|
686
|
-
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
687
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
|
|
688
|
-
const confirmResponse = await fetchFromComms("/files", {
|
|
689
|
-
method: "POST",
|
|
690
|
-
body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
|
|
694
|
+
const sendMessage = (0, import_react.useCallback)(
|
|
695
|
+
async (content, msgOptions = {}) => {
|
|
696
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
697
|
+
const currentSession = sessionRef.current;
|
|
698
|
+
if (!currentActiveChannelId || !content.trim() || !currentSession) return;
|
|
699
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
700
|
+
const trimmedContent = content.trim();
|
|
701
|
+
const optimisticMessage = {
|
|
702
|
+
id: tempId,
|
|
703
|
+
tempId,
|
|
704
|
+
channel_id: currentActiveChannelId,
|
|
705
|
+
sender_id: currentSession.comms_user_id,
|
|
706
|
+
content: trimmedContent,
|
|
707
|
+
type: msgOptions.type || "text",
|
|
708
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
709
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
710
|
+
status: "sending",
|
|
711
|
+
metadata: msgOptions.metadata || {}
|
|
712
|
+
};
|
|
713
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
714
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
715
|
+
setChannels((prev) => {
|
|
716
|
+
const updated = prev.map(
|
|
717
|
+
(ch) => ch.id === currentActiveChannelId ? {
|
|
718
|
+
...ch,
|
|
719
|
+
last_message: {
|
|
720
|
+
id: tempId,
|
|
721
|
+
content: trimmedContent,
|
|
722
|
+
created_at: now,
|
|
723
|
+
sender: {
|
|
724
|
+
id: currentSession.comms_user_id,
|
|
725
|
+
display_name: "You",
|
|
726
|
+
status: "online"
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} : ch
|
|
730
|
+
);
|
|
731
|
+
return updated.sort((a, b) => {
|
|
732
|
+
const timeA = a.last_message?.created_at || "";
|
|
733
|
+
const timeB = b.last_message?.created_at || "";
|
|
734
|
+
return timeB.localeCompare(timeA);
|
|
735
|
+
});
|
|
691
736
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
status: "sending",
|
|
718
|
-
metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
|
|
719
|
-
};
|
|
720
|
-
setMessages((prev) => [...prev, optimisticMessage]);
|
|
721
|
-
try {
|
|
722
|
-
const uploadedFiles = [];
|
|
723
|
-
for (const file of files) {
|
|
724
|
-
const attachment = await uploadFile(file);
|
|
725
|
-
if (attachment) uploadedFiles.push(attachment);
|
|
737
|
+
try {
|
|
738
|
+
await fetchFromComms(
|
|
739
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
740
|
+
{
|
|
741
|
+
method: "POST",
|
|
742
|
+
body: JSON.stringify({
|
|
743
|
+
content: trimmedContent,
|
|
744
|
+
type: msgOptions.type || "text",
|
|
745
|
+
parent_id: msgOptions.parent_id,
|
|
746
|
+
metadata: msgOptions.metadata
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error("[AegisChat] Failed to send message:", error);
|
|
752
|
+
setMessages(
|
|
753
|
+
(prev) => prev.map(
|
|
754
|
+
(m) => m.tempId === tempId ? {
|
|
755
|
+
...m,
|
|
756
|
+
status: "failed",
|
|
757
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
758
|
+
} : m
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
throw error;
|
|
726
762
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
763
|
+
},
|
|
764
|
+
[fetchFromComms]
|
|
765
|
+
);
|
|
766
|
+
const uploadFile = (0, import_react.useCallback)(
|
|
767
|
+
async (file) => {
|
|
768
|
+
const currentSession = sessionRef.current;
|
|
769
|
+
if (!currentSession) return null;
|
|
770
|
+
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
771
|
+
setUploadProgress((prev) => [
|
|
772
|
+
...prev,
|
|
773
|
+
{ fileId, fileName: file.name, progress: 0, status: "pending" }
|
|
774
|
+
]);
|
|
775
|
+
try {
|
|
776
|
+
setUploadProgress(
|
|
777
|
+
(prev) => prev.map(
|
|
778
|
+
(p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
|
|
782
|
+
method: "POST",
|
|
783
|
+
body: JSON.stringify({
|
|
784
|
+
file_name: file.name,
|
|
785
|
+
file_type: file.type || "application/octet-stream",
|
|
786
|
+
file_size: file.size
|
|
787
|
+
})
|
|
788
|
+
});
|
|
789
|
+
setUploadProgress(
|
|
790
|
+
(prev) => prev.map(
|
|
791
|
+
(p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
|
|
795
|
+
method: "PUT",
|
|
796
|
+
body: file,
|
|
797
|
+
headers: { "Content-Type": file.type || "application/octet-stream" }
|
|
798
|
+
});
|
|
799
|
+
if (!uploadResponse.ok)
|
|
800
|
+
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
801
|
+
setUploadProgress(
|
|
802
|
+
(prev) => prev.map(
|
|
803
|
+
(p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
const confirmResponse = await fetchFromComms(
|
|
807
|
+
"/files",
|
|
808
|
+
{
|
|
809
|
+
method: "POST",
|
|
810
|
+
body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
setUploadProgress(
|
|
814
|
+
(prev) => prev.map(
|
|
815
|
+
(p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p
|
|
816
|
+
)
|
|
817
|
+
);
|
|
818
|
+
setTimeout(
|
|
819
|
+
() => setUploadProgress(
|
|
820
|
+
(prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)
|
|
821
|
+
),
|
|
822
|
+
2e3
|
|
823
|
+
);
|
|
824
|
+
return confirmResponse.file;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error("[AegisChat] Failed to upload file:", error);
|
|
827
|
+
setUploadProgress(
|
|
828
|
+
(prev) => prev.map(
|
|
829
|
+
(p) => p.fileId === fileId ? {
|
|
830
|
+
...p,
|
|
831
|
+
status: "error",
|
|
832
|
+
error: error instanceof Error ? error.message : "Upload failed"
|
|
833
|
+
} : p
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
setTimeout(
|
|
837
|
+
() => setUploadProgress(
|
|
838
|
+
(prev) => prev.filter((p) => p.fileId !== fileId)
|
|
839
|
+
),
|
|
840
|
+
5e3
|
|
841
|
+
);
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
[fetchFromComms]
|
|
846
|
+
);
|
|
847
|
+
const sendMessageWithFiles = (0, import_react.useCallback)(
|
|
848
|
+
async (content, files, msgOptions = {}) => {
|
|
849
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
850
|
+
const currentSession = sessionRef.current;
|
|
851
|
+
if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession)
|
|
852
|
+
return;
|
|
853
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
854
|
+
const trimmedContent = content.trim();
|
|
855
|
+
const optimisticMessage = {
|
|
856
|
+
id: tempId,
|
|
857
|
+
tempId,
|
|
858
|
+
channel_id: currentActiveChannelId,
|
|
859
|
+
sender_id: currentSession.comms_user_id,
|
|
860
|
+
content: trimmedContent || `Uploading ${files.length} file(s)...`,
|
|
861
|
+
type: "file",
|
|
862
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
863
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
864
|
+
status: "sending",
|
|
865
|
+
metadata: {
|
|
866
|
+
...msgOptions.metadata,
|
|
867
|
+
files: files.map((f) => ({
|
|
868
|
+
id: `temp-${f.name}`,
|
|
869
|
+
filename: f.name,
|
|
870
|
+
mime_type: f.type,
|
|
871
|
+
size: f.size,
|
|
872
|
+
url: ""
|
|
873
|
+
}))
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
877
|
+
try {
|
|
878
|
+
const uploadedFiles = [];
|
|
879
|
+
for (const file of files) {
|
|
880
|
+
const attachment = await uploadFile(file);
|
|
881
|
+
if (attachment) uploadedFiles.push(attachment);
|
|
882
|
+
}
|
|
883
|
+
const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
|
|
884
|
+
await fetchFromComms(
|
|
885
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
886
|
+
{
|
|
887
|
+
method: "POST",
|
|
888
|
+
body: JSON.stringify({
|
|
889
|
+
content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
|
|
890
|
+
type: msgOptions.type || messageType,
|
|
891
|
+
parent_id: msgOptions.parent_id,
|
|
892
|
+
metadata: { ...msgOptions.metadata, files: uploadedFiles },
|
|
893
|
+
file_ids: uploadedFiles.map((f) => f.id)
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
console.error("[AegisChat] Failed to send message with files:", error);
|
|
899
|
+
setMessages(
|
|
900
|
+
(prev) => prev.map(
|
|
901
|
+
(m) => m.tempId === tempId ? {
|
|
902
|
+
...m,
|
|
903
|
+
status: "failed",
|
|
904
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
905
|
+
} : m
|
|
906
|
+
)
|
|
907
|
+
);
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
[fetchFromComms, uploadFile]
|
|
912
|
+
);
|
|
744
913
|
const stopTyping = (0, import_react.useCallback)(() => {
|
|
745
914
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
746
915
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
747
|
-
wsRef.current.send(
|
|
916
|
+
wsRef.current.send(
|
|
917
|
+
JSON.stringify({
|
|
918
|
+
type: "typing.stop",
|
|
919
|
+
payload: { channel_id: currentActiveChannelId }
|
|
920
|
+
})
|
|
921
|
+
);
|
|
748
922
|
if (typingTimeout.current) {
|
|
749
923
|
clearTimeout(typingTimeout.current);
|
|
750
924
|
typingTimeout.current = null;
|
|
@@ -753,46 +927,106 @@ function useChat(options) {
|
|
|
753
927
|
const startTyping = (0, import_react.useCallback)(() => {
|
|
754
928
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
755
929
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
756
|
-
wsRef.current.send(
|
|
930
|
+
wsRef.current.send(
|
|
931
|
+
JSON.stringify({
|
|
932
|
+
type: "typing.start",
|
|
933
|
+
payload: { channel_id: currentActiveChannelId }
|
|
934
|
+
})
|
|
935
|
+
);
|
|
757
936
|
if (typingTimeout.current) clearTimeout(typingTimeout.current);
|
|
758
937
|
typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
|
|
759
938
|
}, [stopTyping]);
|
|
760
|
-
const createDMWithUser = (0, import_react.useCallback)(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
939
|
+
const createDMWithUser = (0, import_react.useCallback)(
|
|
940
|
+
async (userId) => {
|
|
941
|
+
try {
|
|
942
|
+
const channel = await fetchFromComms("/channels/dm", {
|
|
943
|
+
method: "POST",
|
|
944
|
+
body: JSON.stringify({ user_id: userId })
|
|
945
|
+
});
|
|
946
|
+
await refreshChannels();
|
|
947
|
+
return channel.id;
|
|
948
|
+
} catch (error) {
|
|
949
|
+
console.error("[AegisChat] Failed to create DM:", error);
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
[fetchFromComms, refreshChannels]
|
|
954
|
+
);
|
|
955
|
+
const retryMessage = (0, import_react.useCallback)(
|
|
956
|
+
async (tempId) => {
|
|
957
|
+
const failedMessage = messages.find(
|
|
958
|
+
(m) => m.tempId === tempId && m.status === "failed"
|
|
959
|
+
);
|
|
960
|
+
const currentActiveChannelId = activeChannelIdRef.current;
|
|
961
|
+
if (!failedMessage || !currentActiveChannelId) return;
|
|
962
|
+
setMessages(
|
|
963
|
+
(prev) => prev.map(
|
|
964
|
+
(m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m
|
|
965
|
+
)
|
|
966
|
+
);
|
|
967
|
+
try {
|
|
968
|
+
await fetchFromComms(
|
|
969
|
+
`/channels/${currentActiveChannelId}/messages`,
|
|
970
|
+
{
|
|
971
|
+
method: "POST",
|
|
972
|
+
body: JSON.stringify({
|
|
973
|
+
content: failedMessage.content,
|
|
974
|
+
type: failedMessage.type,
|
|
975
|
+
metadata: failedMessage.metadata
|
|
976
|
+
})
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
console.error("[AegisChat] Failed to retry message:", error);
|
|
981
|
+
setMessages(
|
|
982
|
+
(prev) => prev.map(
|
|
983
|
+
(m) => m.tempId === tempId ? {
|
|
984
|
+
...m,
|
|
985
|
+
status: "failed",
|
|
986
|
+
errorMessage: error instanceof Error ? error.message : "Failed to send"
|
|
987
|
+
} : m
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
[messages, fetchFromComms]
|
|
993
|
+
);
|
|
788
994
|
const deleteFailedMessage = (0, import_react.useCallback)((tempId) => {
|
|
789
995
|
setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
|
|
790
996
|
}, []);
|
|
997
|
+
const setup = (0, import_react.useCallback)((options2) => {
|
|
998
|
+
const {
|
|
999
|
+
config: config2,
|
|
1000
|
+
role: role2,
|
|
1001
|
+
clientId: clientId2,
|
|
1002
|
+
initialSession: initialSession2,
|
|
1003
|
+
autoConnect: autoConnect2 = true,
|
|
1004
|
+
onMessage: onMessage2,
|
|
1005
|
+
onTyping: onTyping2,
|
|
1006
|
+
onConnectionChange: onConnectionChange2
|
|
1007
|
+
} = options2;
|
|
1008
|
+
roleRef.current = role2;
|
|
1009
|
+
clientIdRef.current = clientId2;
|
|
1010
|
+
autoConnectRef.current = autoConnect2;
|
|
1011
|
+
onMessageRef.current = onMessage2;
|
|
1012
|
+
onTypingRef.current = onTyping2;
|
|
1013
|
+
onConnectionChangeRef.current = onConnectionChange2;
|
|
1014
|
+
if (initialSession2) {
|
|
1015
|
+
sessionRef.current = initialSession2;
|
|
1016
|
+
if (!config2) {
|
|
1017
|
+
configureApiClient({
|
|
1018
|
+
baseUrl: initialSession2.api_url.replace(/\/api\/v1\/?$/, ""),
|
|
1019
|
+
getAccessToken: async () => sessionRef.current?.access_token || ""
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
setSession(initialSession2);
|
|
1023
|
+
}
|
|
1024
|
+
}, []);
|
|
791
1025
|
(0, import_react.useEffect)(() => {
|
|
792
|
-
if (
|
|
1026
|
+
if (session && !isConnected && !isConnecting && autoConnectRef.current) {
|
|
793
1027
|
connectWebSocket();
|
|
794
1028
|
}
|
|
795
|
-
}, [
|
|
1029
|
+
}, [session, isConnected, isConnecting, connectWebSocket]);
|
|
796
1030
|
(0, import_react.useEffect)(() => {
|
|
797
1031
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
798
1032
|
wsRef.current.onmessage = (event) => {
|
|
@@ -800,7 +1034,10 @@ function useChat(options) {
|
|
|
800
1034
|
const data = JSON.parse(event.data);
|
|
801
1035
|
handleWebSocketMessage(data);
|
|
802
1036
|
} catch (error) {
|
|
803
|
-
console.error(
|
|
1037
|
+
console.error(
|
|
1038
|
+
"[AegisChat] Failed to parse WebSocket message:",
|
|
1039
|
+
error
|
|
1040
|
+
);
|
|
804
1041
|
}
|
|
805
1042
|
};
|
|
806
1043
|
}
|
|
@@ -852,7 +1089,8 @@ function useChat(options) {
|
|
|
852
1089
|
createDMWithUser,
|
|
853
1090
|
retryMessage,
|
|
854
1091
|
deleteFailedMessage,
|
|
855
|
-
markAsRead
|
|
1092
|
+
markAsRead,
|
|
1093
|
+
setup
|
|
856
1094
|
};
|
|
857
1095
|
}
|
|
858
1096
|
|