@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/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.custom_data?.attachment_id ?? null);
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 (see `useCallAudio` in `@poolse/react-native`).
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.custom_data?.attachment_id ?? null);
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 (see `useCallAudio` in `@poolse/react-native`).
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