@poolse/react 3.0.0 → 3.2.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +71 -293
- package/dist/index.cjs +90 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -3
- package/dist/index.d.ts +25 -3
- package/dist/index.js +90 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.d.cts
CHANGED
|
@@ -324,6 +324,28 @@ interface UseTypingState {
|
|
|
324
324
|
*/
|
|
325
325
|
declare function useTyping(conversationId: string): UseTypingState;
|
|
326
326
|
|
|
327
|
+
/** An AI agent's reply that's still being written. */
|
|
328
|
+
interface MessageStream {
|
|
329
|
+
/** Becomes the finished message's id. */
|
|
330
|
+
id: Uuid;
|
|
331
|
+
conversation_id: Uuid;
|
|
332
|
+
sender_id: Uuid;
|
|
333
|
+
sender_external_id: string | null;
|
|
334
|
+
/** The text so far; empty until the first delta. */
|
|
335
|
+
text: string;
|
|
336
|
+
status: 'streaming' | 'failed';
|
|
337
|
+
error?: string;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* AI agent replies being written in `conversationId`, oldest first.
|
|
341
|
+
*
|
|
342
|
+
* Built from `message:stream` events: `started` adds a stream, each
|
|
343
|
+
* `delta` appends text, `failed` marks it failed (removed after a few
|
|
344
|
+
* seconds). When the finished reply arrives as `message:new` with the
|
|
345
|
+
* same id, the stream is removed — `useMessages` shows the message.
|
|
346
|
+
*/
|
|
347
|
+
declare function useMessageStreams(conversationId: string): MessageStream[];
|
|
348
|
+
|
|
327
349
|
interface UsePresenceState {
|
|
328
350
|
/**
|
|
329
351
|
* Set of `external_id`s currently online in this conversation —
|
|
@@ -438,7 +460,7 @@ interface UseAttachmentUrlState {
|
|
|
438
460
|
* re-keys on a timer.
|
|
439
461
|
*
|
|
440
462
|
* ```tsx
|
|
441
|
-
* const { url } = useAttachmentUrl(message.
|
|
463
|
+
* const { url } = useAttachmentUrl(message.attachments?.[0]?.id ?? null);
|
|
442
464
|
* return url ? <img src={url} /> : <Spinner />;
|
|
443
465
|
* ```
|
|
444
466
|
*/
|
|
@@ -525,7 +547,7 @@ interface UseCallRoom {
|
|
|
525
547
|
* earpiece, but it does have several microphones and a pair of
|
|
526
548
|
* headphones, and choosing between them is the same job. Sparse on
|
|
527
549
|
* React Native, where routing goes through the platform audio session
|
|
528
|
-
* instead (
|
|
550
|
+
* instead (`setAudioOutput` in `@poolse/native`).
|
|
529
551
|
*/
|
|
530
552
|
devices: Record<CallDeviceKind, CallDevice[]>;
|
|
531
553
|
/** Device id in use per kind, where the platform reports one. */
|
|
@@ -692,4 +714,4 @@ declare function canEditOwnMessage(args: {
|
|
|
692
714
|
now?: number;
|
|
693
715
|
}): boolean;
|
|
694
716
|
|
|
695
|
-
export { EDIT_WINDOW_MS, type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCallRoom, type UseCallRoomOptions, type UseLiveCall, type UseReadReceipts, type UseRoom, type UseRoomOptions, type UseUserState, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
|
717
|
+
export { EDIT_WINDOW_MS, type GroupPosition, type MessageStream, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCallRoom, type UseCallRoomOptions, type UseLiveCall, type UseReadReceipts, type UseRoom, type UseRoomOptions, type UseUserState, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessageStreams, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
package/dist/index.d.ts
CHANGED
|
@@ -324,6 +324,28 @@ interface UseTypingState {
|
|
|
324
324
|
*/
|
|
325
325
|
declare function useTyping(conversationId: string): UseTypingState;
|
|
326
326
|
|
|
327
|
+
/** An AI agent's reply that's still being written. */
|
|
328
|
+
interface MessageStream {
|
|
329
|
+
/** Becomes the finished message's id. */
|
|
330
|
+
id: Uuid;
|
|
331
|
+
conversation_id: Uuid;
|
|
332
|
+
sender_id: Uuid;
|
|
333
|
+
sender_external_id: string | null;
|
|
334
|
+
/** The text so far; empty until the first delta. */
|
|
335
|
+
text: string;
|
|
336
|
+
status: 'streaming' | 'failed';
|
|
337
|
+
error?: string;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* AI agent replies being written in `conversationId`, oldest first.
|
|
341
|
+
*
|
|
342
|
+
* Built from `message:stream` events: `started` adds a stream, each
|
|
343
|
+
* `delta` appends text, `failed` marks it failed (removed after a few
|
|
344
|
+
* seconds). When the finished reply arrives as `message:new` with the
|
|
345
|
+
* same id, the stream is removed — `useMessages` shows the message.
|
|
346
|
+
*/
|
|
347
|
+
declare function useMessageStreams(conversationId: string): MessageStream[];
|
|
348
|
+
|
|
327
349
|
interface UsePresenceState {
|
|
328
350
|
/**
|
|
329
351
|
* Set of `external_id`s currently online in this conversation —
|
|
@@ -438,7 +460,7 @@ interface UseAttachmentUrlState {
|
|
|
438
460
|
* re-keys on a timer.
|
|
439
461
|
*
|
|
440
462
|
* ```tsx
|
|
441
|
-
* const { url } = useAttachmentUrl(message.
|
|
463
|
+
* const { url } = useAttachmentUrl(message.attachments?.[0]?.id ?? null);
|
|
442
464
|
* return url ? <img src={url} /> : <Spinner />;
|
|
443
465
|
* ```
|
|
444
466
|
*/
|
|
@@ -525,7 +547,7 @@ interface UseCallRoom {
|
|
|
525
547
|
* earpiece, but it does have several microphones and a pair of
|
|
526
548
|
* headphones, and choosing between them is the same job. Sparse on
|
|
527
549
|
* React Native, where routing goes through the platform audio session
|
|
528
|
-
* instead (
|
|
550
|
+
* instead (`setAudioOutput` in `@poolse/native`).
|
|
529
551
|
*/
|
|
530
552
|
devices: Record<CallDeviceKind, CallDevice[]>;
|
|
531
553
|
/** Device id in use per kind, where the platform reports one. */
|
|
@@ -692,4 +714,4 @@ declare function canEditOwnMessage(args: {
|
|
|
692
714
|
now?: number;
|
|
693
715
|
}): boolean;
|
|
694
716
|
|
|
695
|
-
export { EDIT_WINDOW_MS, type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCallRoom, type UseCallRoomOptions, type UseLiveCall, type UseReadReceipts, type UseRoom, type UseRoomOptions, type UseUserState, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
|
717
|
+
export { EDIT_WINDOW_MS, type GroupPosition, type MessageStream, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCallRoom, type UseCallRoomOptions, type UseLiveCall, type UseReadReceipts, type UseRoom, type UseRoomOptions, type UseUserState, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessageStreams, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
package/dist/index.js
CHANGED
|
@@ -172,9 +172,21 @@ function useConversations() {
|
|
|
172
172
|
const fromMe = by_user_id === me.id;
|
|
173
173
|
setConversations((prev) => mergeUpdated(prev, conversation, fromMe));
|
|
174
174
|
});
|
|
175
|
+
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
176
|
+
let connectedBefore = wasConnected;
|
|
177
|
+
const offStatus = chat.realtime.onStatus((status) => {
|
|
178
|
+
const reconnected = status === "connected" && !wasConnected && connectedBefore;
|
|
179
|
+
connectedBefore ||= status === "connected";
|
|
180
|
+
if (reconnected) {
|
|
181
|
+
chat.conversations.list().then((list) => setConversations(list.data)).catch(() => {
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
wasConnected = status === "connected";
|
|
185
|
+
});
|
|
175
186
|
return () => {
|
|
176
187
|
offCreated();
|
|
177
188
|
offUpdated();
|
|
189
|
+
offStatus();
|
|
178
190
|
};
|
|
179
191
|
}, [chat, me]);
|
|
180
192
|
const markConversationRead = useCallback((conversationId) => {
|
|
@@ -381,11 +393,24 @@ function useMessages(conversationId) {
|
|
|
381
393
|
return updated;
|
|
382
394
|
});
|
|
383
395
|
});
|
|
396
|
+
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
397
|
+
let connectedBefore = wasConnected;
|
|
398
|
+
const offStatus = chat.realtime.onStatus((status) => {
|
|
399
|
+
const reconnected = status === "connected" && !wasConnected && connectedBefore;
|
|
400
|
+
wasConnected = status === "connected";
|
|
401
|
+
connectedBefore ||= wasConnected;
|
|
402
|
+
if (!reconnected || cancelled) return;
|
|
403
|
+
handle.list({ limit: PAGE_SIZE }).then((page) => {
|
|
404
|
+
if (!cancelled) setMessages((prev) => mergeLatest(prev, page.data));
|
|
405
|
+
}).catch(() => {
|
|
406
|
+
});
|
|
407
|
+
});
|
|
384
408
|
return () => {
|
|
385
409
|
cancelled = true;
|
|
386
410
|
offNew();
|
|
387
411
|
offUpdated();
|
|
388
412
|
offDeleted();
|
|
413
|
+
offStatus();
|
|
389
414
|
};
|
|
390
415
|
}, [chat, conversationId]);
|
|
391
416
|
const loadMore = useCallback(async () => {
|
|
@@ -526,6 +551,11 @@ function upsertById(prev, msg) {
|
|
|
526
551
|
next[idx] = msg;
|
|
527
552
|
return next;
|
|
528
553
|
}
|
|
554
|
+
function mergeLatest(prev, page) {
|
|
555
|
+
let next = prev;
|
|
556
|
+
for (const msg of page) next = upsertById(next, msg);
|
|
557
|
+
return next === prev ? prev : [...next].sort((a, b) => a.sequence - b.sequence);
|
|
558
|
+
}
|
|
529
559
|
function useReactions(messageId, opts) {
|
|
530
560
|
const chat = usePoolse();
|
|
531
561
|
const [reactions, setReactions] = useState(opts.initialReactions ?? {});
|
|
@@ -823,6 +853,62 @@ function useTyping(conversationId) {
|
|
|
823
853
|
}, [chat, conversationId]);
|
|
824
854
|
return { typing, signalTyping };
|
|
825
855
|
}
|
|
856
|
+
var FAILED_TTL_MS = 8e3;
|
|
857
|
+
var STALE_MS = 9e4;
|
|
858
|
+
function useMessageStreams(conversationId) {
|
|
859
|
+
const chat = usePoolse();
|
|
860
|
+
const [streams, setStreams] = useState([]);
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
setStreams([]);
|
|
863
|
+
const timers = /* @__PURE__ */ new Map();
|
|
864
|
+
const remove = (id) => {
|
|
865
|
+
const t = timers.get(id);
|
|
866
|
+
if (t) clearTimeout(t);
|
|
867
|
+
timers.delete(id);
|
|
868
|
+
setStreams(
|
|
869
|
+
(prev) => prev.some((s) => s.id === id) ? prev.filter((s) => s.id !== id) : prev
|
|
870
|
+
);
|
|
871
|
+
};
|
|
872
|
+
const arm = (id, ms) => {
|
|
873
|
+
const t = timers.get(id);
|
|
874
|
+
if (t) clearTimeout(t);
|
|
875
|
+
timers.set(
|
|
876
|
+
id,
|
|
877
|
+
setTimeout(() => remove(id), ms)
|
|
878
|
+
);
|
|
879
|
+
};
|
|
880
|
+
const conv = chat.realtime.conversation(conversationId);
|
|
881
|
+
const offStream = conv.onMessageStream((evt) => {
|
|
882
|
+
setStreams((prev) => {
|
|
883
|
+
const existing = prev.find((s) => s.id === evt.id);
|
|
884
|
+
const base = existing ?? {
|
|
885
|
+
id: evt.id,
|
|
886
|
+
conversation_id: evt.conversation_id,
|
|
887
|
+
sender_id: evt.sender_id,
|
|
888
|
+
sender_external_id: evt.sender_external_id ?? null,
|
|
889
|
+
text: "",
|
|
890
|
+
status: "streaming"
|
|
891
|
+
};
|
|
892
|
+
let next;
|
|
893
|
+
if (evt.status === "delta") next = { ...base, text: base.text + (evt.delta ?? "") };
|
|
894
|
+
else if (evt.status === "failed")
|
|
895
|
+
next = { ...base, status: "failed", ...evt.error ? { error: evt.error } : {} };
|
|
896
|
+
else
|
|
897
|
+
next = { ...base, sender_external_id: evt.sender_external_id ?? base.sender_external_id };
|
|
898
|
+
return existing ? prev.map((s) => s.id === evt.id ? next : s) : [...prev, next];
|
|
899
|
+
});
|
|
900
|
+
arm(evt.id, evt.status === "failed" ? FAILED_TTL_MS : STALE_MS);
|
|
901
|
+
});
|
|
902
|
+
const offMessage = conv.onMessage((msg) => remove(msg.id));
|
|
903
|
+
return () => {
|
|
904
|
+
offStream();
|
|
905
|
+
offMessage();
|
|
906
|
+
timers.forEach((t) => clearTimeout(t));
|
|
907
|
+
timers.clear();
|
|
908
|
+
};
|
|
909
|
+
}, [chat, conversationId]);
|
|
910
|
+
return streams;
|
|
911
|
+
}
|
|
826
912
|
function externalIdsFrom(state) {
|
|
827
913
|
const out = [];
|
|
828
914
|
for (const presence of Object.values(state)) {
|
|
@@ -1275,9 +1361,11 @@ function useLiveCall(conversationId) {
|
|
|
1275
1361
|
apply(null);
|
|
1276
1362
|
});
|
|
1277
1363
|
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
1364
|
+
let connectedBefore = wasConnected;
|
|
1278
1365
|
const offStatus = chat.realtime.onStatus((status) => {
|
|
1279
|
-
if (status === "connected" && !wasConnected) refresh();
|
|
1366
|
+
if (status === "connected" && !wasConnected && connectedBefore) refresh();
|
|
1280
1367
|
wasConnected = status === "connected";
|
|
1368
|
+
connectedBefore ||= wasConnected;
|
|
1281
1369
|
});
|
|
1282
1370
|
return () => {
|
|
1283
1371
|
offStarted();
|
|
@@ -1366,6 +1454,6 @@ function canEditOwnMessage(args) {
|
|
|
1366
1454
|
return Number.isFinite(sent) && (args.now ?? Date.now()) - sent < EDIT_WINDOW_MS;
|
|
1367
1455
|
}
|
|
1368
1456
|
|
|
1369
|
-
export { EDIT_WINDOW_MS, PoolseProvider, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
|
1457
|
+
export { EDIT_WINDOW_MS, PoolseProvider, canEditOwnMessage, computeGroupPosition, formatDayLabel, receiptsOf, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCallRoom, useConversation, useConversations, useLiveCall, useMe, useMembers, useMessageStreams, useMessages, usePoolse, usePresence, useReactions, useReadReceipts, useRealtimeStatus, useRoom, useThread, useTyping, useUser };
|
|
1370
1458
|
//# sourceMappingURL=index.js.map
|
|
1371
1459
|
//# sourceMappingURL=index.js.map
|