@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.mjs
CHANGED
|
@@ -35,7 +35,7 @@ var chatApi = {
|
|
|
35
35
|
* Connect to chat session
|
|
36
36
|
*/
|
|
37
37
|
async connect(params, signal) {
|
|
38
|
-
return fetchWithAuth("/
|
|
38
|
+
return fetchWithAuth("/chat/connect", {
|
|
39
39
|
method: "POST",
|
|
40
40
|
body: JSON.stringify(params),
|
|
41
41
|
signal
|
|
@@ -45,7 +45,7 @@ var chatApi = {
|
|
|
45
45
|
* Refresh access token
|
|
46
46
|
*/
|
|
47
47
|
async refreshToken(refreshToken, signal) {
|
|
48
|
-
return fetchWithAuth("/
|
|
48
|
+
return fetchWithAuth("/chat/refresh", {
|
|
49
49
|
method: "POST",
|
|
50
50
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
51
51
|
signal
|
|
@@ -61,19 +61,19 @@ var channelsApi = {
|
|
|
61
61
|
if (options.type) params.append("type", options.type);
|
|
62
62
|
if (options.limit) params.append("limit", String(options.limit));
|
|
63
63
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
64
|
-
return fetchWithAuth(`/
|
|
64
|
+
return fetchWithAuth(`/channels${query}`, { signal });
|
|
65
65
|
},
|
|
66
66
|
/**
|
|
67
67
|
* Get channel by ID
|
|
68
68
|
*/
|
|
69
69
|
async get(channelId, signal) {
|
|
70
|
-
return fetchWithAuth(`/
|
|
70
|
+
return fetchWithAuth(`/channels/${channelId}`, { signal });
|
|
71
71
|
},
|
|
72
72
|
/**
|
|
73
73
|
* Get or create DM channel
|
|
74
74
|
*/
|
|
75
75
|
async getOrCreateDM(userId, signal) {
|
|
76
|
-
return fetchWithAuth("/
|
|
76
|
+
return fetchWithAuth("/channels/dm", {
|
|
77
77
|
method: "POST",
|
|
78
78
|
body: JSON.stringify({ user_id: userId }),
|
|
79
79
|
signal
|
|
@@ -83,7 +83,7 @@ var channelsApi = {
|
|
|
83
83
|
* Create channel
|
|
84
84
|
*/
|
|
85
85
|
async create(data, signal) {
|
|
86
|
-
return fetchWithAuth("/
|
|
86
|
+
return fetchWithAuth("/channels", {
|
|
87
87
|
method: "POST",
|
|
88
88
|
body: JSON.stringify(data),
|
|
89
89
|
signal
|
|
@@ -93,7 +93,7 @@ var channelsApi = {
|
|
|
93
93
|
* Mark channel as read
|
|
94
94
|
*/
|
|
95
95
|
async markAsRead(channelId, signal) {
|
|
96
|
-
return fetchWithAuth(`/
|
|
96
|
+
return fetchWithAuth(`/channels/${channelId}/read`, {
|
|
97
97
|
method: "POST",
|
|
98
98
|
signal
|
|
99
99
|
});
|
|
@@ -102,13 +102,13 @@ var channelsApi = {
|
|
|
102
102
|
* Get channel members
|
|
103
103
|
*/
|
|
104
104
|
async getMembers(channelId, signal) {
|
|
105
|
-
return fetchWithAuth(`/
|
|
105
|
+
return fetchWithAuth(`/channels/${channelId}/members`, { signal });
|
|
106
106
|
},
|
|
107
107
|
/**
|
|
108
108
|
* Update channel
|
|
109
109
|
*/
|
|
110
110
|
async update(channelId, data, signal) {
|
|
111
|
-
return fetchWithAuth(`/
|
|
111
|
+
return fetchWithAuth(`/channels/${channelId}`, {
|
|
112
112
|
method: "PATCH",
|
|
113
113
|
body: JSON.stringify(data),
|
|
114
114
|
signal
|
|
@@ -124,13 +124,13 @@ var messagesApi = {
|
|
|
124
124
|
if (options.limit) params.append("limit", String(options.limit));
|
|
125
125
|
if (options.before) params.append("before", options.before);
|
|
126
126
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
127
|
-
return fetchWithAuth(`/
|
|
127
|
+
return fetchWithAuth(`/channels/${channelId}/messages${query}`, { signal });
|
|
128
128
|
},
|
|
129
129
|
/**
|
|
130
130
|
* Send a message
|
|
131
131
|
*/
|
|
132
132
|
async send(channelId, data, signal) {
|
|
133
|
-
return fetchWithAuth(`/
|
|
133
|
+
return fetchWithAuth(`/channels/${channelId}/messages`, {
|
|
134
134
|
method: "POST",
|
|
135
135
|
body: JSON.stringify(data),
|
|
136
136
|
signal
|
|
@@ -140,7 +140,7 @@ var messagesApi = {
|
|
|
140
140
|
* Update a message
|
|
141
141
|
*/
|
|
142
142
|
async update(channelId, messageId, data, signal) {
|
|
143
|
-
return fetchWithAuth(`/
|
|
143
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}`, {
|
|
144
144
|
method: "PATCH",
|
|
145
145
|
body: JSON.stringify(data),
|
|
146
146
|
signal
|
|
@@ -150,7 +150,7 @@ var messagesApi = {
|
|
|
150
150
|
* Delete a message
|
|
151
151
|
*/
|
|
152
152
|
async delete(channelId, messageId, signal) {
|
|
153
|
-
return fetchWithAuth(`/
|
|
153
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}`, {
|
|
154
154
|
method: "DELETE",
|
|
155
155
|
signal
|
|
156
156
|
});
|
|
@@ -159,7 +159,7 @@ var messagesApi = {
|
|
|
159
159
|
* Mark messages as delivered
|
|
160
160
|
*/
|
|
161
161
|
async markDelivered(channelId, signal) {
|
|
162
|
-
return fetchWithAuth(`/
|
|
162
|
+
return fetchWithAuth(`/channels/${channelId}/messages/delivered`, {
|
|
163
163
|
method: "POST",
|
|
164
164
|
signal
|
|
165
165
|
});
|
|
@@ -168,7 +168,7 @@ var messagesApi = {
|
|
|
168
168
|
* Mark messages as read
|
|
169
169
|
*/
|
|
170
170
|
async markRead(channelId, signal) {
|
|
171
|
-
return fetchWithAuth(`/
|
|
171
|
+
return fetchWithAuth(`/channels/${channelId}/messages/read`, {
|
|
172
172
|
method: "POST",
|
|
173
173
|
signal
|
|
174
174
|
});
|
|
@@ -179,7 +179,7 @@ var reactionsApi = {
|
|
|
179
179
|
* Add reaction to a message
|
|
180
180
|
*/
|
|
181
181
|
async add(channelId, messageId, emoji, signal) {
|
|
182
|
-
return fetchWithAuth(`/
|
|
182
|
+
return fetchWithAuth(`/channels/${channelId}/messages/${messageId}/reactions`, {
|
|
183
183
|
method: "POST",
|
|
184
184
|
body: JSON.stringify({ emoji }),
|
|
185
185
|
signal
|
|
@@ -190,7 +190,7 @@ var reactionsApi = {
|
|
|
190
190
|
*/
|
|
191
191
|
async remove(channelId, messageId, emoji, signal) {
|
|
192
192
|
return fetchWithAuth(
|
|
193
|
-
`/
|
|
193
|
+
`/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`,
|
|
194
194
|
{ method: "DELETE", signal }
|
|
195
195
|
);
|
|
196
196
|
}
|
|
@@ -200,7 +200,7 @@ var filesApi = {
|
|
|
200
200
|
* Get upload URL
|
|
201
201
|
*/
|
|
202
202
|
async getUploadUrl(data, signal) {
|
|
203
|
-
return fetchWithAuth("/
|
|
203
|
+
return fetchWithAuth("/files/upload-url", {
|
|
204
204
|
method: "POST",
|
|
205
205
|
body: JSON.stringify(data),
|
|
206
206
|
signal
|
|
@@ -210,7 +210,7 @@ var filesApi = {
|
|
|
210
210
|
* Confirm file upload
|
|
211
211
|
*/
|
|
212
212
|
async confirm(fileId, signal) {
|
|
213
|
-
return fetchWithAuth("/
|
|
213
|
+
return fetchWithAuth("/files", {
|
|
214
214
|
method: "POST",
|
|
215
215
|
body: JSON.stringify({ file_id: fileId }),
|
|
216
216
|
signal
|
|
@@ -220,7 +220,7 @@ var filesApi = {
|
|
|
220
220
|
* Get download URL
|
|
221
221
|
*/
|
|
222
222
|
async getDownloadUrl(fileId, signal) {
|
|
223
|
-
return fetchWithAuth(`/
|
|
223
|
+
return fetchWithAuth(`/files/${fileId}/download`, { signal });
|
|
224
224
|
}
|
|
225
225
|
};
|
|
226
226
|
var usersApi = {
|
|
@@ -228,13 +228,13 @@ var usersApi = {
|
|
|
228
228
|
* Search users
|
|
229
229
|
*/
|
|
230
230
|
async search(query, signal) {
|
|
231
|
-
return fetchWithAuth(`/
|
|
231
|
+
return fetchWithAuth(`/users/search?q=${encodeURIComponent(query)}`, { signal });
|
|
232
232
|
},
|
|
233
233
|
/**
|
|
234
234
|
* Get user by ID
|
|
235
235
|
*/
|
|
236
236
|
async get(userId, signal) {
|
|
237
|
-
return fetchWithAuth(`/
|
|
237
|
+
return fetchWithAuth(`/users/${userId}`, { signal });
|
|
238
238
|
}
|
|
239
239
|
};
|
|
240
240
|
|
|
@@ -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,23 +279,19 @@ 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
|
-
}
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
configRef.current = config;
|
|
279
|
-
}, [config]);
|
|
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);
|
|
280
289
|
useEffect(() => {
|
|
281
290
|
activeChannelIdRef.current = activeChannelId;
|
|
282
291
|
}, [activeChannelId]);
|
|
283
292
|
useEffect(() => {
|
|
284
|
-
|
|
285
|
-
}, [
|
|
293
|
+
activeChannelIdRef.current = activeChannelId;
|
|
294
|
+
}, [activeChannelId]);
|
|
286
295
|
const getActiveChannelId = useCallback(() => {
|
|
287
296
|
if (typeof window === "undefined") return null;
|
|
288
297
|
return sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -297,26 +306,29 @@ function useChat(options) {
|
|
|
297
306
|
}
|
|
298
307
|
}
|
|
299
308
|
}, []);
|
|
300
|
-
const fetchFromComms = useCallback(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const response = await fetch(`${currentSession.api_url}${path}`, {
|
|
306
|
-
...fetchOptions,
|
|
307
|
-
headers: {
|
|
308
|
-
"Content-Type": "application/json",
|
|
309
|
-
Authorization: `Bearer ${currentSession.access_token}`,
|
|
310
|
-
...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");
|
|
311
314
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
);
|
|
320
332
|
const clearTimers = useCallback(() => {
|
|
321
333
|
if (reconnectTimeout.current) {
|
|
322
334
|
clearTimeout(reconnectTimeout.current);
|
|
@@ -327,111 +339,141 @@ function useChat(options) {
|
|
|
327
339
|
pingInterval.current = null;
|
|
328
340
|
}
|
|
329
341
|
}, []);
|
|
330
|
-
const handleWebSocketMessage = useCallback(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
340
380
|
);
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
if (prev.some((m) => m.id === newMessage.id)) return prev;
|
|
347
|
-
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
|
+
});
|
|
348
386
|
});
|
|
349
|
-
|
|
387
|
+
break;
|
|
350
388
|
}
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
(ch) => ch.id === newMessage.channel_id ? {
|
|
354
|
-
...ch,
|
|
355
|
-
last_message: {
|
|
356
|
-
id: newMessage.id,
|
|
357
|
-
content: newMessage.content,
|
|
358
|
-
created_at: newMessage.created_at,
|
|
359
|
-
sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
|
|
360
|
-
},
|
|
361
|
-
unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
|
|
362
|
-
} : ch
|
|
363
|
-
);
|
|
364
|
-
return updated.sort((a, b) => {
|
|
365
|
-
const timeA = a.last_message?.created_at || "";
|
|
366
|
-
const timeB = b.last_message?.created_at || "";
|
|
367
|
-
return timeB.localeCompare(timeA);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
case "message.updated": {
|
|
373
|
-
const updatedMessage = data.payload;
|
|
374
|
-
setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
case "message.deleted": {
|
|
378
|
-
const { message_id } = data.payload;
|
|
379
|
-
setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
case "message.delivered":
|
|
383
|
-
case "message.read": {
|
|
384
|
-
const { message_id, channel_id, status } = data.payload;
|
|
385
|
-
if (channel_id === currentActiveChannelId) {
|
|
389
|
+
case "message.updated": {
|
|
390
|
+
const updatedMessage = data.payload;
|
|
386
391
|
setMessages(
|
|
387
|
-
(prev) => prev.map((m) => m.id ===
|
|
392
|
+
(prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
|
|
388
393
|
);
|
|
394
|
+
break;
|
|
389
395
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
case "message.delivered.batch":
|
|
393
|
-
case "message.read.batch": {
|
|
394
|
-
const { channel_id } = data.payload;
|
|
395
|
-
if (channel_id === currentActiveChannelId) {
|
|
396
|
+
case "message.deleted": {
|
|
397
|
+
const { message_id } = data.payload;
|
|
396
398
|
setMessages(
|
|
397
|
-
(prev) => prev.map(
|
|
399
|
+
(prev) => prev.map(
|
|
400
|
+
(m) => m.id === message_id ? { ...m, deleted: true } : m
|
|
401
|
+
)
|
|
398
402
|
);
|
|
403
|
+
break;
|
|
399
404
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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);
|
|
424
467
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.log("[AegisChat] Unhandled message type:", data.type);
|
|
429
|
-
}
|
|
430
|
-
}, [onMessage, onTyping]);
|
|
468
|
+
},
|
|
469
|
+
[]
|
|
470
|
+
);
|
|
431
471
|
const connectWebSocket = useCallback(() => {
|
|
432
472
|
const currentSession = sessionRef.current;
|
|
433
473
|
if (!currentSession?.websocket_url || !currentSession?.access_token) {
|
|
434
|
-
console.warn(
|
|
474
|
+
console.warn(
|
|
475
|
+
"[AegisChat] Cannot connect WebSocket - missing session or token"
|
|
476
|
+
);
|
|
435
477
|
return;
|
|
436
478
|
}
|
|
437
479
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
@@ -448,14 +490,19 @@ function useChat(options) {
|
|
|
448
490
|
setIsConnected(true);
|
|
449
491
|
setIsConnecting(false);
|
|
450
492
|
reconnectAttempts.current = 0;
|
|
451
|
-
|
|
493
|
+
onConnectionChangeRef.current?.(true);
|
|
452
494
|
pingInterval.current = setInterval(() => {
|
|
453
495
|
if (ws.readyState === WebSocket.OPEN) {
|
|
454
496
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
455
497
|
}
|
|
456
498
|
}, PING_INTERVAL);
|
|
457
499
|
if (activeChannelIdRef.current) {
|
|
458
|
-
ws.send(
|
|
500
|
+
ws.send(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
type: "channel.join",
|
|
503
|
+
payload: { channel_id: activeChannelIdRef.current }
|
|
504
|
+
})
|
|
505
|
+
);
|
|
459
506
|
}
|
|
460
507
|
};
|
|
461
508
|
ws.onmessage = (event) => {
|
|
@@ -471,9 +518,12 @@ function useChat(options) {
|
|
|
471
518
|
setIsConnected(false);
|
|
472
519
|
setIsConnecting(false);
|
|
473
520
|
clearTimers();
|
|
474
|
-
|
|
521
|
+
onConnectionChangeRef.current?.(false);
|
|
475
522
|
if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
|
|
476
|
-
const delay = Math.min(
|
|
523
|
+
const delay = Math.min(
|
|
524
|
+
RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current),
|
|
525
|
+
MAX_RECONNECT_DELAY
|
|
526
|
+
);
|
|
477
527
|
console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
|
|
478
528
|
reconnectTimeout.current = setTimeout(() => {
|
|
479
529
|
reconnectAttempts.current++;
|
|
@@ -485,14 +535,18 @@ function useChat(options) {
|
|
|
485
535
|
console.error("[AegisChat] WebSocket error:", error);
|
|
486
536
|
};
|
|
487
537
|
wsRef.current = ws;
|
|
488
|
-
}, [clearTimers, handleWebSocketMessage
|
|
538
|
+
}, [clearTimers, handleWebSocketMessage]);
|
|
489
539
|
const connect = useCallback(async () => {
|
|
490
540
|
console.log("[AegisChat] connect() called");
|
|
491
|
-
const targetSession = sessionRef.current
|
|
541
|
+
const targetSession = sessionRef.current;
|
|
492
542
|
if (!targetSession) {
|
|
493
543
|
console.log("[AegisChat] No session available, skipping connect");
|
|
494
544
|
return;
|
|
495
545
|
}
|
|
546
|
+
if (!autoConnectRef.current) {
|
|
547
|
+
console.log("[AegisChat] autoConnect is false, skipping connect");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
496
550
|
connectWebSocket();
|
|
497
551
|
}, [connectWebSocket]);
|
|
498
552
|
const disconnect = useCallback(() => {
|
|
@@ -520,48 +574,72 @@ function useChat(options) {
|
|
|
520
574
|
setIsLoadingChannels(false);
|
|
521
575
|
}
|
|
522
576
|
}, []);
|
|
523
|
-
const selectChannel = useCallback(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
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
|
+
);
|
|
532
599
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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);
|
|
542
621
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
console.error("[AegisChat] Failed to mark as read:", error);
|
|
557
|
-
}
|
|
558
|
-
}, [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
|
+
);
|
|
559
635
|
const loadMoreMessages = useCallback(async () => {
|
|
560
636
|
if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
|
|
561
637
|
setIsLoadingMessages(true);
|
|
562
638
|
try {
|
|
563
639
|
const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
|
|
564
|
-
const response = await fetchFromComms(
|
|
640
|
+
const response = await fetchFromComms(
|
|
641
|
+
`/channels/${activeChannelId}/messages${params}`
|
|
642
|
+
);
|
|
565
643
|
setMessages((prev) => [...response.messages || [], ...prev]);
|
|
566
644
|
setHasMoreMessages(response.has_more);
|
|
567
645
|
if (response.oldest_id) {
|
|
@@ -573,138 +651,234 @@ function useChat(options) {
|
|
|
573
651
|
setIsLoadingMessages(false);
|
|
574
652
|
}
|
|
575
653
|
}, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
|
|
576
|
-
const sendMessage = useCallback(
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
});
|
|
619
|
-
} catch (error) {
|
|
620
|
-
console.error("[AegisChat] Failed to send message:", error);
|
|
621
|
-
setMessages(
|
|
622
|
-
(prev) => prev.map(
|
|
623
|
-
(m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
|
|
624
|
-
)
|
|
625
|
-
);
|
|
626
|
-
throw error;
|
|
627
|
-
}
|
|
628
|
-
}, [fetchFromComms]);
|
|
629
|
-
const uploadFile = useCallback(async (file) => {
|
|
630
|
-
const currentSession = sessionRef.current;
|
|
631
|
-
if (!currentSession) return null;
|
|
632
|
-
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
633
|
-
setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
|
|
634
|
-
try {
|
|
635
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
|
|
636
|
-
const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
|
|
637
|
-
method: "POST",
|
|
638
|
-
body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
|
|
639
|
-
});
|
|
640
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
|
|
641
|
-
const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
|
|
642
|
-
method: "PUT",
|
|
643
|
-
body: file,
|
|
644
|
-
headers: { "Content-Type": file.type || "application/octet-stream" }
|
|
645
|
-
});
|
|
646
|
-
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
647
|
-
setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
|
|
648
|
-
const confirmResponse = await fetchFromComms("/files", {
|
|
649
|
-
method: "POST",
|
|
650
|
-
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
|
+
});
|
|
651
696
|
});
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
status: "sending",
|
|
678
|
-
metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
|
|
679
|
-
};
|
|
680
|
-
setMessages((prev) => [...prev, optimisticMessage]);
|
|
681
|
-
try {
|
|
682
|
-
const uploadedFiles = [];
|
|
683
|
-
for (const file of files) {
|
|
684
|
-
const attachment = await uploadFile(file);
|
|
685
|
-
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;
|
|
686
722
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
+
);
|
|
704
873
|
const stopTyping = useCallback(() => {
|
|
705
874
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
706
875
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
707
|
-
wsRef.current.send(
|
|
876
|
+
wsRef.current.send(
|
|
877
|
+
JSON.stringify({
|
|
878
|
+
type: "typing.stop",
|
|
879
|
+
payload: { channel_id: currentActiveChannelId }
|
|
880
|
+
})
|
|
881
|
+
);
|
|
708
882
|
if (typingTimeout.current) {
|
|
709
883
|
clearTimeout(typingTimeout.current);
|
|
710
884
|
typingTimeout.current = null;
|
|
@@ -713,46 +887,106 @@ function useChat(options) {
|
|
|
713
887
|
const startTyping = useCallback(() => {
|
|
714
888
|
const currentActiveChannelId = activeChannelIdRef.current;
|
|
715
889
|
if (!currentActiveChannelId || !wsRef.current) return;
|
|
716
|
-
wsRef.current.send(
|
|
890
|
+
wsRef.current.send(
|
|
891
|
+
JSON.stringify({
|
|
892
|
+
type: "typing.start",
|
|
893
|
+
payload: { channel_id: currentActiveChannelId }
|
|
894
|
+
})
|
|
895
|
+
);
|
|
717
896
|
if (typingTimeout.current) clearTimeout(typingTimeout.current);
|
|
718
897
|
typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
|
|
719
898
|
}, [stopTyping]);
|
|
720
|
-
const createDMWithUser = useCallback(
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
+
);
|
|
748
954
|
const deleteFailedMessage = useCallback((tempId) => {
|
|
749
955
|
setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
|
|
750
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.replace(/\/api\/v1\/?$/, ""),
|
|
979
|
+
getAccessToken: async () => sessionRef.current?.access_token || ""
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
setSession(initialSession2);
|
|
983
|
+
}
|
|
984
|
+
}, []);
|
|
751
985
|
useEffect(() => {
|
|
752
|
-
if (
|
|
986
|
+
if (session && !isConnected && !isConnecting && autoConnectRef.current) {
|
|
753
987
|
connectWebSocket();
|
|
754
988
|
}
|
|
755
|
-
}, [
|
|
989
|
+
}, [session, isConnected, isConnecting, connectWebSocket]);
|
|
756
990
|
useEffect(() => {
|
|
757
991
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
758
992
|
wsRef.current.onmessage = (event) => {
|
|
@@ -760,7 +994,10 @@ function useChat(options) {
|
|
|
760
994
|
const data = JSON.parse(event.data);
|
|
761
995
|
handleWebSocketMessage(data);
|
|
762
996
|
} catch (error) {
|
|
763
|
-
console.error(
|
|
997
|
+
console.error(
|
|
998
|
+
"[AegisChat] Failed to parse WebSocket message:",
|
|
999
|
+
error
|
|
1000
|
+
);
|
|
764
1001
|
}
|
|
765
1002
|
};
|
|
766
1003
|
}
|
|
@@ -812,7 +1049,8 @@ function useChat(options) {
|
|
|
812
1049
|
createDMWithUser,
|
|
813
1050
|
retryMessage,
|
|
814
1051
|
deleteFailedMessage,
|
|
815
|
-
markAsRead
|
|
1052
|
+
markAsRead,
|
|
1053
|
+
setup
|
|
816
1054
|
};
|
|
817
1055
|
}
|
|
818
1056
|
|