@poolse/react 2.0.12 → 2.1.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.ts CHANGED
@@ -181,6 +181,15 @@ interface UseMessagesState {
181
181
  * "tell the server we read up to here".
182
182
  */
183
183
  markReadUpTo: (messageId: Uuid) => Promise<void>;
184
+ /**
185
+ * Re-fetch the newest page (PAGE_SIZE) and replace the cached
186
+ * messages. Useful when the app returns from background or the
187
+ * realtime socket reconnects after a drop — any messages we missed
188
+ * while disconnected are pulled in via REST. Pagination resets:
189
+ * `hasMore` becomes true again so `loadMore()` will walk back from
190
+ * the new tail.
191
+ */
192
+ refetch: () => Promise<void>;
184
193
  }
185
194
  /**
186
195
  * Live message list for `conversationId`. Combines:
@@ -303,8 +312,15 @@ interface UseTypingState {
303
312
  }
304
313
  /**
305
314
  * Tracks who's typing in `conversationId`. Subscribes to the
306
- * `typing:start` / `typing:stop` broadcasts; auto-cleans up entries
307
- * when stop fires.
315
+ * `typing:start` / `typing:stop` broadcasts, plus two purely
316
+ * client-side eviction paths that keep the chip from lingering when
317
+ * the server is slow (or never) to send `typing:stop`:
318
+ *
319
+ * 1. Per-user inactivity timer — each `typing:start` resets a 4 s
320
+ * timeout for that `external_id`; when it fires we drop them
321
+ * from the set. Covers "they stopped typing without sending".
322
+ * 2. `message:new` from the same sender — if a typing user sends a
323
+ * message, we evict them immediately. Covers "they just sent".
308
324
  */
309
325
  declare function useTyping(conversationId: string): UseTypingState;
310
326
 
@@ -448,4 +464,25 @@ interface UseUserState {
448
464
  */
449
465
  declare function useUser(externalId: string | null | undefined): UseUserState;
450
466
 
451
- export { PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseUserState, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
467
+ /**
468
+ * Where this message sits inside a same-sender, same-day, in-window
469
+ * cluster. Drives the bubble's corner treatment + the row's
470
+ * inter-cluster margin.
471
+ *
472
+ * * `standalone` — single bubble, gets the tail
473
+ * * `first` — first of multi; all corners rounded
474
+ * * `middle` — interior; all corners rounded
475
+ * * `last` — last of multi; gets the tail
476
+ */
477
+ type GroupPosition = 'first' | 'middle' | 'last' | 'standalone';
478
+ declare function computeGroupPosition(msg: Message, prev: Message | null, next: Message | null, windowMs: number): GroupPosition;
479
+ declare function sameGroup(a: Message | null, b: Message | null, windowMs: number): boolean;
480
+ declare function sameDay(a: string, b: string): boolean;
481
+ /**
482
+ * "Today" / "Yesterday" / "Mon, 26 May" / "26 May 2024" — same
483
+ * convention as iMessage / WhatsApp / Telegram. `now` is exposed
484
+ * for deterministic testing; defaults to the current wall clock.
485
+ */
486
+ declare function formatDayLabel(iso: string, now?: Date): string;
487
+
488
+ export { type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseUserState, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
package/dist/index.js CHANGED
@@ -315,6 +315,7 @@ function useMessages(conversationId) {
315
315
  const [error, setError] = useState(null);
316
316
  const [hasMore, setHasMore] = useState(true);
317
317
  const oldestSequenceRef = useRef(null);
318
+ const refetchRef = useRef(null);
318
319
  const meIdRef = useRef(me?.id ?? null);
319
320
  meIdRef.current = me?.id ?? null;
320
321
  const meExternalIdRef = useRef(me?.external_id ?? null);
@@ -339,6 +340,13 @@ function useMessages(conversationId) {
339
340
  setError(err instanceof Error ? err : new Error(String(err)));
340
341
  setLoading(false);
341
342
  });
343
+ refetchRef.current = async () => {
344
+ const page = await handle.list({ limit: PAGE_SIZE });
345
+ const ordered = [...page.data].reverse();
346
+ setMessages(ordered);
347
+ setHasMore(page.data.length === PAGE_SIZE);
348
+ oldestSequenceRef.current = ordered[0]?.sequence ?? null;
349
+ };
342
350
  const conv = chat.realtime.conversation(conversationId);
343
351
  const offNew = conv.onMessage((msg) => {
344
352
  if (msg.thread_root_id) {
@@ -488,6 +496,11 @@ function useMessages(conversationId) {
488
496
  },
489
497
  [chat, conversationId]
490
498
  );
499
+ const refetch = useCallback(async () => {
500
+ const fn = refetchRef.current;
501
+ if (!fn) return;
502
+ await fn();
503
+ }, []);
491
504
  return {
492
505
  messages,
493
506
  loading,
@@ -497,7 +510,8 @@ function useMessages(conversationId) {
497
510
  send,
498
511
  edit,
499
512
  delete: deleteMessage,
500
- markReadUpTo
513
+ markReadUpTo,
514
+ refetch
501
515
  };
502
516
  }
503
517
  function upsertById(prev, msg) {
@@ -734,6 +748,7 @@ function upsertById2(prev, msg) {
734
748
  next[idx] = msg;
735
749
  return next;
736
750
  }
751
+ var TYPING_TIMEOUT_MS = 4e3;
737
752
  function useTyping(conversationId) {
738
753
  const chat = usePoolse();
739
754
  const { me } = useMe();
@@ -743,11 +758,31 @@ function useTyping(conversationId) {
743
758
  meExtRef.current = me?.external_id ?? null;
744
759
  useEffect(() => {
745
760
  setTyping(/* @__PURE__ */ new Set());
761
+ const timers = /* @__PURE__ */ new Map();
762
+ const evict = (ext) => {
763
+ const t = timers.get(ext);
764
+ if (t) {
765
+ clearTimeout(t);
766
+ timers.delete(ext);
767
+ }
768
+ setTyping((prev) => {
769
+ if (!prev.has(ext)) return prev;
770
+ const next = new Set(prev);
771
+ next.delete(ext);
772
+ return next;
773
+ });
774
+ };
746
775
  const conv = chat.realtime.conversation(conversationId);
747
776
  const offStart = conv.onTypingStart((evt) => {
748
777
  const ext = evt.external_id;
749
778
  if (!ext) return;
750
779
  if (meExtRef.current && ext === meExtRef.current) return;
780
+ const existing = timers.get(ext);
781
+ if (existing) clearTimeout(existing);
782
+ timers.set(
783
+ ext,
784
+ setTimeout(() => evict(ext), TYPING_TIMEOUT_MS)
785
+ );
751
786
  setTyping((prev) => {
752
787
  if (prev.has(ext)) return prev;
753
788
  const next = new Set(prev);
@@ -759,16 +794,20 @@ function useTyping(conversationId) {
759
794
  const ext = evt.external_id;
760
795
  if (!ext) return;
761
796
  if (meExtRef.current && ext === meExtRef.current) return;
762
- setTyping((prev) => {
763
- if (!prev.has(ext)) return prev;
764
- const next = new Set(prev);
765
- next.delete(ext);
766
- return next;
767
- });
797
+ evict(ext);
798
+ });
799
+ const offMessage = conv.onMessage((msg) => {
800
+ const ext = msg.sender_external_id;
801
+ if (!ext) return;
802
+ if (meExtRef.current && ext === meExtRef.current) return;
803
+ evict(ext);
768
804
  });
769
805
  return () => {
770
806
  offStart();
771
807
  offStop();
808
+ offMessage();
809
+ for (const t of timers.values()) clearTimeout(t);
810
+ timers.clear();
772
811
  };
773
812
  }, [chat, conversationId]);
774
813
  const signalTyping = useCallback(() => {
@@ -985,6 +1024,43 @@ function useUser(externalId) {
985
1024
  return state;
986
1025
  }
987
1026
 
988
- export { PoolseProvider, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
1027
+ // src/grouping.ts
1028
+ function computeGroupPosition(msg, prev, next, windowMs) {
1029
+ const continuesPrev = sameGroup(msg, prev, windowMs);
1030
+ const continuesNext = sameGroup(next, msg, windowMs);
1031
+ if (continuesPrev && continuesNext) return "middle";
1032
+ if (continuesPrev) return "last";
1033
+ if (continuesNext) return "first";
1034
+ return "standalone";
1035
+ }
1036
+ function sameGroup(a, b, windowMs) {
1037
+ if (!a || !b) return false;
1038
+ if (a.sender_id !== b.sender_id) return false;
1039
+ if (!sameDay(a.inserted_at, b.inserted_at)) return false;
1040
+ const ta = new Date(a.inserted_at).getTime();
1041
+ const tb = new Date(b.inserted_at).getTime();
1042
+ return Math.abs(ta - tb) <= windowMs;
1043
+ }
1044
+ function sameDay(a, b) {
1045
+ const da = new Date(a);
1046
+ const db = new Date(b);
1047
+ return da.getFullYear() === db.getFullYear() && da.getMonth() === db.getMonth() && da.getDate() === db.getDate();
1048
+ }
1049
+ function formatDayLabel(iso, now = /* @__PURE__ */ new Date()) {
1050
+ const d = new Date(iso);
1051
+ const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
1052
+ const diffDays = Math.floor((startOfDay(now) - startOfDay(d)) / 864e5);
1053
+ if (diffDays === 0) return "Today";
1054
+ if (diffDays === 1) return "Yesterday";
1055
+ if (diffDays < 7) {
1056
+ return d.toLocaleDateString(void 0, { weekday: "long" });
1057
+ }
1058
+ if (d.getFullYear() === now.getFullYear()) {
1059
+ return d.toLocaleDateString(void 0, { weekday: "short", day: "numeric", month: "short" });
1060
+ }
1061
+ return d.toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
1062
+ }
1063
+
1064
+ export { PoolseProvider, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
989
1065
  //# sourceMappingURL=index.js.map
990
1066
  //# sourceMappingURL=index.js.map