@poolse/react 1.0.0-beta.0 → 1.0.0-rc.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/README.md CHANGED
@@ -49,21 +49,43 @@ function Chat({ conversationId }: { conversationId: string }) {
49
49
 
50
50
  ## Hooks at a glance
51
51
 
52
- | Hook | Returns |
53
- | ------------------------------- | ----------------------------------------------------------------- |
54
- | `useMe()` | The signed-in End User |
55
- | `useConversations()` | List + `create` + realtime new-conversation push |
56
- | `useConversation(id)` | Single conversation read + refetch |
57
- | `useMembers(convId)` | Membership list + add/remove helpers |
58
- | `useMessages(convId)` | Live messages + `send` + pagination + optimistic dedup |
59
- | `useThread(convId, rootId)` | Replies + `sendReply` + pagination |
60
- | `useTyping(convId)` | Live `typing` set + `signalTyping` (debounced) |
61
- | `usePresence(convId)` | Live `online` set |
62
- | `useReactions(messageId, opts)` | Live reaction map + add/remove |
63
- | `useAttachmentUpload()` | Stateful `upload(file)` with `uploading` + `error` |
64
- | `useAttachmentUrl(id)` | Presigned download URL |
65
- | `useRealtimeStatus()` | Socket lifecycle (connecting / connected / reconnecting / closed) |
66
- | `usePoolse()` | Raw `Poolse` instance (escape hatch) |
52
+ | Hook | Returns |
53
+ | ------------------------------- | ------------------------------------------------------------------------------------ |
54
+ | `useMe()` | The signed-in End User |
55
+ | `useConversations()` | List + `create` + realtime new-conversation push |
56
+ | `useConversation(id)` | Single conversation read + refetch |
57
+ | `useMembers(convId)` | Membership list + add/remove helpers |
58
+ | `useMessages(convId)` | Live messages + `send` + pagination + optimistic dedup |
59
+ | `useThread(convId, rootId)` | Replies + `sendReply` + pagination |
60
+ | `useTyping(convId)` | Live `typing` set + `signalTyping` (debounced) |
61
+ | `usePresence(convId)` | Live `online` set |
62
+ | `useReactions(messageId, opts)` | Live reaction map + add/remove |
63
+ | `useAttachmentUpload()` | Upload queue with per-item progress, `upload` / `uploadAll` / `cancel` / `remove` |
64
+ | `useAttachmentUrl(id)` | Presigned download URL |
65
+ | `useUser(userId)` | Customer-supplied profile via `config.userResolver`, cached + dedup'd |
66
+ | `useRealtimeStatus()` | Socket lifecycle (connecting / connected / reconnecting / closed) |
67
+ | `usePoolse()` | Raw `Poolse` instance (escape hatch) |
68
+
69
+ ## Identity resolution (`userResolver`)
70
+
71
+ The SDK doesn't know your users' names or where their avatars live. Wire a `userResolver` once on the provider and every component that renders a sender (`<MessageBubble>`, `<MemberList>`, `<TypingIndicator>`, `<MentionInput>`, …) lights up automatically:
72
+
73
+ ```tsx
74
+ <PoolseProvider
75
+ config={{
76
+ apiUrl,
77
+ getToken,
78
+ userResolver: async (userId) => {
79
+ const u = await fetch(`/api/users/by-poolse-id/${userId}`).then((r) => r.json());
80
+ return { displayName: u.full_name, avatarUrl: u.avatar_url };
81
+ },
82
+ }}
83
+ >
84
+ …
85
+ </PoolseProvider>
86
+ ```
87
+
88
+ Resolved profiles are cached in-memory and concurrent lookups dedupe — a busy chat with 50 messages from 5 senders fires the resolver 5 times, not 50.
67
89
 
68
90
  ## What the provider gives you
69
91
 
package/dist/index.cjs CHANGED
@@ -32,7 +32,11 @@ function PoolseProvider({ config, children }) {
32
32
  ...initial.generateIdempotencyKey ? { generateIdempotencyKey: () => configRef.current.generateIdempotencyKey() } : {},
33
33
  ...initial.wsUrl !== void 0 ? { wsUrl: initial.wsUrl } : {},
34
34
  ...initial.socketPath !== void 0 ? { socketPath: initial.socketPath } : {},
35
- ...initial.onSocketError ? { onSocketError: (err) => configRef.current.onSocketError(err) } : {}
35
+ ...initial.onSocketError ? { onSocketError: (err) => configRef.current.onSocketError(err) } : {},
36
+ // Customer's user metadata resolver — name + avatar lookup.
37
+ // Read through the ref so the customer can swap the closure
38
+ // (e.g. switch tenant context) without re-mounting.
39
+ ...initial.userResolver ? { userResolver: (id) => configRef.current.userResolver(id) } : {}
36
40
  });
37
41
  }, []);
38
42
  if (process.env.NODE_ENV !== "production") {
@@ -224,7 +228,6 @@ function useMembers(conversationId) {
224
228
  hasFetchedRef.current = true;
225
229
  } catch (err) {
226
230
  if (err?.name === "AbortError") return;
227
- console.error("[poolse] useMembers fetch failed:", err);
228
231
  setError(err instanceof Error ? err : new Error(String(err)));
229
232
  } finally {
230
233
  setLoading(false);
@@ -782,45 +785,107 @@ function useRealtimeStatus() {
782
785
  }
783
786
  function useAttachmentUpload() {
784
787
  const chat = usePoolse();
785
- const [attachment, setAttachment] = react.useState(null);
786
- const [uploading, setUploading] = react.useState(false);
787
- const [error, setError] = react.useState(null);
788
- const inFlightRef = react.useRef(null);
789
- const upload = react.useCallback(
790
- async (input) => {
791
- inFlightRef.current?.abort();
788
+ const [queue, setQueue] = react.useState([]);
789
+ const inFlightRef = react.useRef(/* @__PURE__ */ new Map());
790
+ const mountedRef = react.useRef(true);
791
+ react.useEffect(() => {
792
+ mountedRef.current = true;
793
+ return () => {
794
+ mountedRef.current = false;
795
+ for (const v of inFlightRef.current.values()) v.controller.abort();
796
+ inFlightRef.current.clear();
797
+ };
798
+ }, []);
799
+ const updateItem = react.useCallback((localId, patch) => {
800
+ if (!mountedRef.current) return;
801
+ setQueue((q) => q.map((it) => it.localId === localId ? { ...it, ...patch } : it));
802
+ }, []);
803
+ const runUpload = react.useCallback(
804
+ async (localId, input) => {
792
805
  const controller = new AbortController();
793
- inFlightRef.current = controller;
794
- setUploading(true);
795
- setError(null);
806
+ inFlightRef.current.set(localId, { controller });
807
+ updateItem(localId, { status: "uploading", loaded: 0 });
796
808
  try {
797
- const att = await chat.attachments.upload(input, { signal: controller.signal });
798
- if (inFlightRef.current === controller) {
799
- setAttachment(att);
800
- }
809
+ const att = await chat.attachments.upload(input, {
810
+ signal: controller.signal,
811
+ onProgress: (e) => updateItem(localId, { loaded: e.loaded })
812
+ });
813
+ updateItem(localId, {
814
+ status: "ready",
815
+ loaded: input.byteSize,
816
+ attachment: att,
817
+ error: null
818
+ });
801
819
  return att;
802
820
  } catch (err) {
803
- if (err?.name === "AbortError") throw err;
821
+ if (err?.name === "AbortError") {
822
+ updateItem(localId, { status: "cancelled" });
823
+ throw err;
824
+ }
804
825
  const e = err instanceof Error ? err : new Error(String(err));
805
- setError(e);
826
+ updateItem(localId, { status: "error", error: e });
806
827
  throw e;
807
828
  } finally {
808
- if (inFlightRef.current === controller) {
809
- setUploading(false);
810
- inFlightRef.current = null;
811
- }
829
+ inFlightRef.current.delete(localId);
812
830
  }
813
831
  },
814
- [chat]
832
+ [chat, updateItem]
833
+ );
834
+ const enqueue = react.useCallback((input) => {
835
+ const localId = generateLocalId();
836
+ const item = {
837
+ localId,
838
+ filename: input.filename ?? "file",
839
+ contentType: input.contentType,
840
+ byteSize: input.byteSize,
841
+ status: "pending",
842
+ loaded: 0,
843
+ attachment: null,
844
+ error: null
845
+ };
846
+ setQueue((q) => [...q, item]);
847
+ return item;
848
+ }, []);
849
+ const upload = react.useCallback(
850
+ (input) => {
851
+ const item = enqueue(input);
852
+ return runUpload(item.localId, input);
853
+ },
854
+ [enqueue, runUpload]
855
+ );
856
+ const uploadAll = react.useCallback(
857
+ (inputs) => {
858
+ const items = inputs.map((inp) => ({ item: enqueue(inp), input: inp }));
859
+ return Promise.all(items.map(({ item, input }) => runUpload(item.localId, input)));
860
+ },
861
+ [enqueue, runUpload]
862
+ );
863
+ const cancel = react.useCallback((localId) => {
864
+ const inf = inFlightRef.current.get(localId);
865
+ if (inf) inf.controller.abort();
866
+ }, []);
867
+ const remove = react.useCallback(
868
+ (localId) => {
869
+ const inf = inFlightRef.current.get(localId);
870
+ if (inf) inf.controller.abort();
871
+ setQueue((q) => q.filter((it) => it.localId !== localId));
872
+ },
873
+ []
815
874
  );
816
875
  const reset = react.useCallback(() => {
817
- inFlightRef.current?.abort();
818
- inFlightRef.current = null;
819
- setAttachment(null);
820
- setError(null);
821
- setUploading(false);
876
+ for (const v of inFlightRef.current.values()) v.controller.abort();
877
+ inFlightRef.current.clear();
878
+ setQueue([]);
822
879
  }, []);
823
- return { attachment, uploading, error, upload, reset };
880
+ const uploading = queue.some((it) => it.status === "pending" || it.status === "uploading");
881
+ const error = queue.reduce((acc, it) => it.error ?? acc, null);
882
+ const attachment = queue.reduce((acc, it) => it.attachment ?? acc, null);
883
+ return { queue, attachment, uploading, error, upload, uploadAll, cancel, remove, reset };
884
+ }
885
+ function generateLocalId() {
886
+ const c = globalThis.crypto;
887
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
888
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
824
889
  }
825
890
  function useAttachmentUrl(attachmentId) {
826
891
  const chat = usePoolse();
@@ -855,6 +920,35 @@ function useAttachmentUrl(attachmentId) {
855
920
  }, [chat, attachmentId]);
856
921
  return { url, loading, error };
857
922
  }
923
+ var EMPTY = { profile: null, loading: false };
924
+ function useUser(userId) {
925
+ const chat = usePoolse();
926
+ const [state, setState] = react.useState(() => {
927
+ if (!userId) return EMPTY;
928
+ const cached = chat.users.peek(userId);
929
+ if (cached === void 0) return { profile: null, loading: true };
930
+ return { profile: cached, loading: false };
931
+ });
932
+ react.useEffect(() => {
933
+ if (!userId) {
934
+ setState(EMPTY);
935
+ return;
936
+ }
937
+ const apply = () => {
938
+ const cached = chat.users.peek(userId);
939
+ if (cached === void 0) {
940
+ setState({ profile: null, loading: true });
941
+ void chat.users.get(userId);
942
+ } else {
943
+ setState({ profile: cached, loading: false });
944
+ }
945
+ };
946
+ const off = chat.users.subscribe(userId, apply);
947
+ apply();
948
+ return off;
949
+ }, [chat, userId]);
950
+ return state;
951
+ }
858
952
 
859
953
  Object.defineProperty(exports, "ApiError", {
860
954
  enumerable: true,
@@ -890,5 +984,6 @@ exports.useReactions = useReactions;
890
984
  exports.useRealtimeStatus = useRealtimeStatus;
891
985
  exports.useThread = useThread;
892
986
  exports.useTyping = useTyping;
987
+ exports.useUser = useUser;
893
988
  //# sourceMappingURL=index.cjs.map
894
989
  //# sourceMappingURL=index.cjs.map