@fluxy-chat/sdk 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/dist/agent-outbound.d.ts +14 -0
- package/dist/agent-outbound.js +53 -0
- package/dist/index.d.ts +22 -51
- package/dist/index.js +54 -448
- package/dist/jwt-utils.d.ts +8 -0
- package/dist/jwt-utils.js +19 -0
- package/dist/message-history.d.ts +23 -0
- package/dist/message-history.js +21 -0
- package/dist/realtime-provider.d.ts +27 -0
- package/dist/realtime-provider.js +131 -0
- package/dist/room-rest.d.ts +4 -0
- package/dist/room-rest.js +24 -0
- package/dist/url-utils.d.ts +2 -0
- package/dist/url-utils.js +7 -0
- package/dist/use-chat.d.ts +49 -0
- package/dist/use-chat.js +482 -0
- package/dist/use-fluxy-chat.d.ts +14 -0
- package/dist/use-fluxy-chat.js +14 -0
- package/dist/use-rooms.d.ts +9 -0
- package/dist/use-rooms.js +25 -0
- package/package.json +23 -2
package/dist/use-chat.js
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { FluxyAuthError, FluxySendError } from "./errors";
|
|
4
|
+
import { mergeMessagesChronological, sortMessagesChronological, } from "./message-history";
|
|
5
|
+
import { useFluxyChatOptional } from "./use-fluxy-chat";
|
|
6
|
+
export function useChat({ roomId, client: clientProp, agentId, historyLimit = 50 }) {
|
|
7
|
+
const realtime = useFluxyChatOptional();
|
|
8
|
+
const client = clientProp ?? realtime?.client ?? null;
|
|
9
|
+
const [messages, setMessages] = React.useState([]);
|
|
10
|
+
const [hasMore, setHasMore] = React.useState(false);
|
|
11
|
+
const [isLoadingMore, setIsLoadingMore] = React.useState(false);
|
|
12
|
+
const [online, setOnline] = React.useState(0);
|
|
13
|
+
const [typingUsers, setTypingUsers] = React.useState({});
|
|
14
|
+
const [seenBy, setSeenBy] = React.useState({});
|
|
15
|
+
const [onlineUsers, setOnlineUsers] = React.useState([]);
|
|
16
|
+
const [connected, setConnected] = React.useState(false);
|
|
17
|
+
const [connectionStatus, setConnectionStatus] = React.useState("connecting");
|
|
18
|
+
const [reconnectAttempt, setReconnectAttempt] = React.useState(0);
|
|
19
|
+
const [connectionError, setConnectionError] = React.useState(null);
|
|
20
|
+
const [agentTyping, setAgentTyping] = React.useState(false);
|
|
21
|
+
const [wsTypingAgentId, setWsTypingAgentId] = React.useState(null);
|
|
22
|
+
const [invokeTypingAgentId, setInvokeTypingAgentId] = React.useState(null);
|
|
23
|
+
const [reactions, setReactions] = React.useState({});
|
|
24
|
+
const connectionRef = React.useRef(null);
|
|
25
|
+
const sseRef = React.useRef(null);
|
|
26
|
+
const pollTimerRef = React.useRef(null);
|
|
27
|
+
const loadMore = React.useCallback(async () => {
|
|
28
|
+
if (!client || isLoadingMore || !hasMore)
|
|
29
|
+
return;
|
|
30
|
+
const trimmedRoomId = roomId.trim();
|
|
31
|
+
if (!trimmedRoomId)
|
|
32
|
+
return;
|
|
33
|
+
const chronological = sortMessagesChronological(messages);
|
|
34
|
+
const oldest = chronological[0];
|
|
35
|
+
if (!oldest?.createdAt)
|
|
36
|
+
return;
|
|
37
|
+
setIsLoadingMore(true);
|
|
38
|
+
try {
|
|
39
|
+
const older = await client.fetchMessages(trimmedRoomId, {
|
|
40
|
+
limit: historyLimit,
|
|
41
|
+
before: oldest.createdAt,
|
|
42
|
+
});
|
|
43
|
+
setMessages((prev) => mergeMessagesChronological(prev, older));
|
|
44
|
+
setHasMore(older.length >= historyLimit);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* keep existing list */
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setIsLoadingMore(false);
|
|
51
|
+
}
|
|
52
|
+
}, [client, hasMore, historyLimit, isLoadingMore, messages, roomId]);
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
let active = true;
|
|
55
|
+
const trimmedRoomId = roomId.trim();
|
|
56
|
+
const MAX_WS_RECONNECT_ATTEMPTS = 6;
|
|
57
|
+
const POLL_INTERVAL_MS = 4000;
|
|
58
|
+
const stopPollingFallback = () => {
|
|
59
|
+
if (pollTimerRef.current) {
|
|
60
|
+
clearInterval(pollTimerRef.current);
|
|
61
|
+
pollTimerRef.current = null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const stopSSEFallback = () => {
|
|
65
|
+
if (sseRef.current) {
|
|
66
|
+
sseRef.current.close();
|
|
67
|
+
sseRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const startPollingFallback = () => {
|
|
71
|
+
if (!client)
|
|
72
|
+
return;
|
|
73
|
+
stopPollingFallback();
|
|
74
|
+
stopSSEFallback();
|
|
75
|
+
const tick = async () => {
|
|
76
|
+
if (!active || !client)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
const next = await client.fetchMessages(trimmedRoomId, { limit: historyLimit });
|
|
80
|
+
if (active) {
|
|
81
|
+
setMessages(next);
|
|
82
|
+
setHasMore(next.length >= historyLimit);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* ignore transient poll errors */
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
void tick();
|
|
90
|
+
pollTimerRef.current = setInterval(tick, POLL_INTERVAL_MS);
|
|
91
|
+
};
|
|
92
|
+
const startSSEFallback = () => {
|
|
93
|
+
if (!client)
|
|
94
|
+
return;
|
|
95
|
+
stopPollingFallback();
|
|
96
|
+
stopSSEFallback();
|
|
97
|
+
const es = client.connectSSE(trimmedRoomId);
|
|
98
|
+
if (!es) {
|
|
99
|
+
startPollingFallback();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
sseRef.current = es;
|
|
103
|
+
setConnectionStatus("sse");
|
|
104
|
+
es.addEventListener("message", (event) => {
|
|
105
|
+
if (!active)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(event.data);
|
|
109
|
+
handleEvent(data);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* ignore malformed SSE events */
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
es.addEventListener("error", () => {
|
|
116
|
+
if (!active)
|
|
117
|
+
return;
|
|
118
|
+
stopSSEFallback();
|
|
119
|
+
startPollingFallback();
|
|
120
|
+
setConnectionStatus("polling");
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const handleEvent = (data) => {
|
|
124
|
+
if (data.type === "history") {
|
|
125
|
+
setMessages((prev) => mergeMessagesChronological(prev, sortMessagesChronological(data.messages)));
|
|
126
|
+
}
|
|
127
|
+
else if (data.type === "message") {
|
|
128
|
+
setMessages((prev) => {
|
|
129
|
+
const idx = prev.findIndex((m) => m.id === data.id);
|
|
130
|
+
if (idx >= 0) {
|
|
131
|
+
const next = [...prev];
|
|
132
|
+
next[idx] = { ...next[idx], ...data };
|
|
133
|
+
return sortMessagesChronological(next);
|
|
134
|
+
}
|
|
135
|
+
return sortMessagesChronological([...prev, data]);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (data.type === "presence") {
|
|
139
|
+
setOnline(data.online);
|
|
140
|
+
if (data.users)
|
|
141
|
+
setOnlineUsers(data.users);
|
|
142
|
+
}
|
|
143
|
+
else if (data.type === "typing") {
|
|
144
|
+
setTypingUsers((prev) => ({
|
|
145
|
+
...prev,
|
|
146
|
+
[data.userId]: data.isTyping,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
else if (data.type === "agentTyping") {
|
|
150
|
+
setAgentTyping(data.isTyping);
|
|
151
|
+
setWsTypingAgentId(data.isTyping ? data.agentId : null);
|
|
152
|
+
}
|
|
153
|
+
else if (data.type === "edit") {
|
|
154
|
+
setMessages((prev) => prev.map((m) => m.id === data.id
|
|
155
|
+
? {
|
|
156
|
+
...m,
|
|
157
|
+
content: data.content,
|
|
158
|
+
editedAt: data.editedAt,
|
|
159
|
+
streaming: data.streaming ?? false,
|
|
160
|
+
}
|
|
161
|
+
: m));
|
|
162
|
+
}
|
|
163
|
+
else if (data.type === "reaction") {
|
|
164
|
+
setReactions((prev) => {
|
|
165
|
+
const byMessage = { ...prev };
|
|
166
|
+
const current = { ...(byMessage[data.messageId] || {}) };
|
|
167
|
+
const existingCount = current[data.emoji] || 0;
|
|
168
|
+
if (data.op === "remove") {
|
|
169
|
+
const nextCount = Math.max(existingCount - 1, 0);
|
|
170
|
+
if (nextCount === 0) {
|
|
171
|
+
delete current[data.emoji];
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
current[data.emoji] = nextCount;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
current[data.emoji] = existingCount + 1;
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(current).length === 0) {
|
|
181
|
+
delete byMessage[data.messageId];
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
byMessage[data.messageId] = current;
|
|
185
|
+
}
|
|
186
|
+
return byMessage;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (data.type === "read") {
|
|
190
|
+
setSeenBy((prev) => {
|
|
191
|
+
const existing = prev[data.messageId] || [];
|
|
192
|
+
if (existing.includes(data.userId))
|
|
193
|
+
return prev;
|
|
194
|
+
return {
|
|
195
|
+
...prev,
|
|
196
|
+
[data.messageId]: [...existing, data.userId],
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (data.type === "delete") {
|
|
201
|
+
if (data.hard) {
|
|
202
|
+
setMessages((prev) => prev.filter((m) => m.id !== data.id));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
setMessages((prev) => prev.map((m) => m.id === data.id
|
|
206
|
+
? { ...m, content: "[deleted]", deletedAt: data.deletedAt }
|
|
207
|
+
: m));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
if (!client || !trimmedRoomId || !client.isAuthenticated()) {
|
|
212
|
+
setMessages([]);
|
|
213
|
+
setHasMore(false);
|
|
214
|
+
setConnected(false);
|
|
215
|
+
setConnectionStatus("disconnected");
|
|
216
|
+
return () => {
|
|
217
|
+
active = false;
|
|
218
|
+
stopPollingFallback();
|
|
219
|
+
stopSSEFallback();
|
|
220
|
+
connectionRef.current?.close();
|
|
221
|
+
connectionRef.current = null;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
client
|
|
225
|
+
.fetchMessages(trimmedRoomId, { limit: historyLimit })
|
|
226
|
+
.then((initial) => {
|
|
227
|
+
if (!active)
|
|
228
|
+
return;
|
|
229
|
+
setMessages(initial);
|
|
230
|
+
setHasMore(initial.length >= historyLimit);
|
|
231
|
+
})
|
|
232
|
+
.catch(() => {
|
|
233
|
+
/* history load is best-effort until member JWT + room are ready */
|
|
234
|
+
});
|
|
235
|
+
const connection = client.connectRoom(trimmedRoomId, {
|
|
236
|
+
maxReconnectAttempts: MAX_WS_RECONNECT_ATTEMPTS,
|
|
237
|
+
historyLimit,
|
|
238
|
+
onStatusChange: (status) => {
|
|
239
|
+
if (!active)
|
|
240
|
+
return;
|
|
241
|
+
if (status === "connected") {
|
|
242
|
+
setConnected(true);
|
|
243
|
+
setConnectionStatus("connected");
|
|
244
|
+
setReconnectAttempt(0);
|
|
245
|
+
setConnectionError(null);
|
|
246
|
+
stopPollingFallback();
|
|
247
|
+
stopSSEFallback();
|
|
248
|
+
}
|
|
249
|
+
else if (status === "connecting") {
|
|
250
|
+
setConnectionStatus("connecting");
|
|
251
|
+
setConnected(false);
|
|
252
|
+
}
|
|
253
|
+
else if (status === "reconnecting") {
|
|
254
|
+
setConnectionStatus("reconnecting");
|
|
255
|
+
setConnected(false);
|
|
256
|
+
setReconnectAttempt(connection.reconnectAttempts);
|
|
257
|
+
}
|
|
258
|
+
else if (status === "disconnected") {
|
|
259
|
+
setConnected(false);
|
|
260
|
+
setConnectionStatus("disconnected");
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
onAuthError: (err) => {
|
|
264
|
+
if (!active)
|
|
265
|
+
return;
|
|
266
|
+
setConnectionError(err);
|
|
267
|
+
setConnected(false);
|
|
268
|
+
setConnectionStatus("disconnected");
|
|
269
|
+
realtime?.refreshSession();
|
|
270
|
+
},
|
|
271
|
+
onConnectionError: (err) => {
|
|
272
|
+
if (!active)
|
|
273
|
+
return;
|
|
274
|
+
if (!(err instanceof FluxyAuthError)) {
|
|
275
|
+
setConnectionError(err);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
onReconnectFailed: () => {
|
|
279
|
+
if (!active)
|
|
280
|
+
return;
|
|
281
|
+
setReconnectAttempt(connection.reconnectAttempts);
|
|
282
|
+
if (client.isAuthenticated()) {
|
|
283
|
+
startSSEFallback();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
startPollingFallback();
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
connection.addEventListener("message", (data) => {
|
|
291
|
+
if (!active)
|
|
292
|
+
return;
|
|
293
|
+
handleEvent(data);
|
|
294
|
+
});
|
|
295
|
+
connectionRef.current = connection;
|
|
296
|
+
connection.connect();
|
|
297
|
+
return () => {
|
|
298
|
+
active = false;
|
|
299
|
+
stopPollingFallback();
|
|
300
|
+
stopSSEFallback();
|
|
301
|
+
connection.close();
|
|
302
|
+
connectionRef.current = null;
|
|
303
|
+
setConnected(false);
|
|
304
|
+
setConnectionStatus("disconnected");
|
|
305
|
+
};
|
|
306
|
+
}, [roomId, client, historyLimit, realtime?.refreshSession]);
|
|
307
|
+
const sendMessage = (content, replyTo, attachments) => {
|
|
308
|
+
if (!client)
|
|
309
|
+
return;
|
|
310
|
+
if (client.isAuthenticated()) {
|
|
311
|
+
void client
|
|
312
|
+
.createMessage(roomId, content, replyTo, attachments)
|
|
313
|
+
.catch((err) =>
|
|
314
|
+
// eslint-disable-next-line no-console
|
|
315
|
+
console.error("[fluxychat] REST sendMessage failed, falling back to WS:", err));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
connectionRef.current?.sendJson({
|
|
320
|
+
type: "message",
|
|
321
|
+
userId: client.userId,
|
|
322
|
+
content,
|
|
323
|
+
parentId: replyTo ?? null,
|
|
324
|
+
attachments: attachments ?? [],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (err instanceof FluxySendError)
|
|
329
|
+
return;
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const setTyping = (isTyping) => {
|
|
334
|
+
if (!client)
|
|
335
|
+
return;
|
|
336
|
+
try {
|
|
337
|
+
connectionRef.current?.sendJson({
|
|
338
|
+
type: "typing",
|
|
339
|
+
userId: client.userId,
|
|
340
|
+
isTyping,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* socket not open */
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const editMessage = (messageId, content) => {
|
|
348
|
+
if (!client)
|
|
349
|
+
return;
|
|
350
|
+
const tryWsEdit = () => {
|
|
351
|
+
try {
|
|
352
|
+
connectionRef.current?.sendJson({
|
|
353
|
+
type: "edit",
|
|
354
|
+
userId: client.userId,
|
|
355
|
+
messageId,
|
|
356
|
+
content,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
/* socket not open */
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
if (client.isAuthenticated()) {
|
|
364
|
+
void client.editMessageRest(messageId, content).catch((err) => {
|
|
365
|
+
// eslint-disable-next-line no-console
|
|
366
|
+
console.error("[fluxychat] REST editMessage failed, falling back to WS:", err);
|
|
367
|
+
tryWsEdit();
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
tryWsEdit();
|
|
372
|
+
};
|
|
373
|
+
const sendReaction = (messageId, emoji, op = "add") => {
|
|
374
|
+
if (!client)
|
|
375
|
+
return;
|
|
376
|
+
if (client.isAuthenticated()) {
|
|
377
|
+
void client
|
|
378
|
+
.sendReactionRest(messageId, emoji, op)
|
|
379
|
+
.catch((err) =>
|
|
380
|
+
// eslint-disable-next-line no-console
|
|
381
|
+
console.error("[fluxychat] REST sendReaction failed, falling back to WS:", err));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
connectionRef.current?.sendJson({
|
|
386
|
+
type: "reaction",
|
|
387
|
+
userId: client.userId,
|
|
388
|
+
messageId,
|
|
389
|
+
emoji,
|
|
390
|
+
op,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
/* socket not open */
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const sendReadReceipt = (messageId) => {
|
|
398
|
+
if (!client)
|
|
399
|
+
return;
|
|
400
|
+
if (client.isAuthenticated()) {
|
|
401
|
+
void client
|
|
402
|
+
.markReadRest(roomId, messageId)
|
|
403
|
+
.catch((err) =>
|
|
404
|
+
// eslint-disable-next-line no-console
|
|
405
|
+
console.error("[fluxychat] REST sendReadReceipt failed, falling back to WS:", err));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
connectionRef.current?.sendJson({
|
|
410
|
+
type: "read",
|
|
411
|
+
userId: client.userId,
|
|
412
|
+
messageId,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
/* socket not open */
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const deleteMessage = (messageId) => {
|
|
420
|
+
if (!client)
|
|
421
|
+
return;
|
|
422
|
+
const tryWsDelete = () => {
|
|
423
|
+
try {
|
|
424
|
+
connectionRef.current?.sendJson({ type: "delete", messageId });
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
/* socket not open */
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
if (client.isAuthenticated()) {
|
|
431
|
+
void client.deleteMessageRest(messageId).catch((err) => {
|
|
432
|
+
// eslint-disable-next-line no-console
|
|
433
|
+
console.error("[fluxychat] REST deleteMessage failed, falling back to WS:", err);
|
|
434
|
+
tryWsDelete();
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
tryWsDelete();
|
|
439
|
+
};
|
|
440
|
+
const invokeAgent = async (content, options) => {
|
|
441
|
+
if (!client) {
|
|
442
|
+
throw new Error("useChat requires a FluxyChatClient or FluxyRealtimeProvider");
|
|
443
|
+
}
|
|
444
|
+
const targetAgentId = options?.agentId || agentId;
|
|
445
|
+
if (!targetAgentId) {
|
|
446
|
+
throw new Error("invokeAgent requires an agentId in hook options or call options");
|
|
447
|
+
}
|
|
448
|
+
setAgentTyping(true);
|
|
449
|
+
try {
|
|
450
|
+
return await client.invokeAgentRest(targetAgentId, roomId, content, {
|
|
451
|
+
replyTo: options?.replyTo,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
setAgentTyping(false);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
messages,
|
|
460
|
+
hasMore,
|
|
461
|
+
isLoadingMore,
|
|
462
|
+
loadMore,
|
|
463
|
+
online,
|
|
464
|
+
typingUsers,
|
|
465
|
+
seenBy,
|
|
466
|
+
onlineUsers,
|
|
467
|
+
connected,
|
|
468
|
+
connectionStatus,
|
|
469
|
+
reconnectAttempt,
|
|
470
|
+
connectionError,
|
|
471
|
+
agentTyping,
|
|
472
|
+
typingAgentId: wsTypingAgentId ?? invokeTypingAgentId,
|
|
473
|
+
reactions,
|
|
474
|
+
sendMessage,
|
|
475
|
+
setTyping,
|
|
476
|
+
editMessage,
|
|
477
|
+
sendReaction,
|
|
478
|
+
sendReadReceipt,
|
|
479
|
+
deleteMessage,
|
|
480
|
+
invokeAgent,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FluxyChatClient } from "./index";
|
|
3
|
+
export interface FluxyRealtimeContextValue {
|
|
4
|
+
client: FluxyChatClient | null;
|
|
5
|
+
userId: string;
|
|
6
|
+
token: string | null;
|
|
7
|
+
workerUrl: string;
|
|
8
|
+
ready: boolean;
|
|
9
|
+
refreshSession: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare const FluxyRealtimeContext: React.Context<FluxyRealtimeContextValue | null>;
|
|
12
|
+
export declare function useFluxyChat(): FluxyRealtimeContextValue;
|
|
13
|
+
/** Returns null when no provider is mounted (useChat can still take an explicit client). */
|
|
14
|
+
export declare function useFluxyChatOptional(): FluxyRealtimeContextValue | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
export const FluxyRealtimeContext = React.createContext(null);
|
|
4
|
+
export function useFluxyChat() {
|
|
5
|
+
const ctx = React.useContext(FluxyRealtimeContext);
|
|
6
|
+
if (!ctx) {
|
|
7
|
+
throw new Error("useFluxyChat must be used within FluxyRealtimeProvider");
|
|
8
|
+
}
|
|
9
|
+
return ctx;
|
|
10
|
+
}
|
|
11
|
+
/** Returns null when no provider is mounted (useChat can still take an explicit client). */
|
|
12
|
+
export function useFluxyChatOptional() {
|
|
13
|
+
return React.useContext(FluxyRealtimeContext);
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FluxyChatClient, FluxyChatRoom } from "./index";
|
|
2
|
+
export declare function useRooms(client: FluxyChatClient): {
|
|
3
|
+
rooms: (FluxyChatRoom & {
|
|
4
|
+
unreadCount?: number;
|
|
5
|
+
})[];
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
reload: () => Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
export function useRooms(client) {
|
|
4
|
+
const [rooms, setRooms] = React.useState([]);
|
|
5
|
+
const [loading, setLoading] = React.useState(false);
|
|
6
|
+
const [error, setError] = React.useState(null);
|
|
7
|
+
const load = async () => {
|
|
8
|
+
setLoading(true);
|
|
9
|
+
setError(null);
|
|
10
|
+
try {
|
|
11
|
+
const next = await client.listRooms();
|
|
12
|
+
setRooms(next);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
setLoading(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
void load();
|
|
23
|
+
}, [client]);
|
|
24
|
+
return { rooms, loading, error, reload: load };
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluxy-chat/sdk",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Fluxychat JavaScript/TypeScript SDK — WebSocket rooms, REST messages, useChat
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Fluxychat JavaScript/TypeScript SDK — WebSocket rooms, REST messages, FluxyRealtimeProvider, useChat with loadMore",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fluxychat",
|
|
7
7
|
"repository": {
|
|
@@ -32,6 +32,24 @@
|
|
|
32
32
|
"dist/errors.d.ts",
|
|
33
33
|
"dist/message-stream.js",
|
|
34
34
|
"dist/message-stream.d.ts",
|
|
35
|
+
"dist/message-history.js",
|
|
36
|
+
"dist/message-history.d.ts",
|
|
37
|
+
"dist/jwt-utils.js",
|
|
38
|
+
"dist/jwt-utils.d.ts",
|
|
39
|
+
"dist/realtime-provider.js",
|
|
40
|
+
"dist/realtime-provider.d.ts",
|
|
41
|
+
"dist/use-fluxy-chat.js",
|
|
42
|
+
"dist/use-fluxy-chat.d.ts",
|
|
43
|
+
"dist/use-chat.js",
|
|
44
|
+
"dist/use-chat.d.ts",
|
|
45
|
+
"dist/use-rooms.js",
|
|
46
|
+
"dist/use-rooms.d.ts",
|
|
47
|
+
"dist/room-rest.js",
|
|
48
|
+
"dist/room-rest.d.ts",
|
|
49
|
+
"dist/agent-outbound.js",
|
|
50
|
+
"dist/agent-outbound.d.ts",
|
|
51
|
+
"dist/url-utils.js",
|
|
52
|
+
"dist/url-utils.d.ts",
|
|
35
53
|
"LICENSE",
|
|
36
54
|
"README.md"
|
|
37
55
|
],
|
|
@@ -51,6 +69,9 @@
|
|
|
51
69
|
"devDependencies": {
|
|
52
70
|
"@types/node": "^24.3.0",
|
|
53
71
|
"@types/react": "^19.2.14",
|
|
72
|
+
"@types/react-dom": "^19.2.3",
|
|
73
|
+
"react": "latest",
|
|
74
|
+
"react-dom": "latest",
|
|
54
75
|
"typescript": "latest",
|
|
55
76
|
"vitest": "^4.1.5"
|
|
56
77
|
}
|