@poolse/react 0.2.0-alpha.7 → 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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput } from '@poolse/sdk';
3
- export { ApiError, Attachment, AttachmentStatus, AttachmentUploadInput, AttachmentUploadRequest, AttachmentUploadResponse, AuthError, Conversation, ConversationCreateRequest, Me, MemberRole, Membership, Message, MessageCreateRequest, MessageType, NetworkError, PoolseConfig, PoolseError, RateLimitedError, RealtimeStatus, Uuid } from '@poolse/sdk';
2
+ import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput, PoolseUserProfile } from '@poolse/sdk';
3
+ export { ApiError, Attachment, AttachmentStatus, AttachmentUploadInput, AttachmentUploadRequest, AttachmentUploadResponse, AuthError, Conversation, ConversationCreateRequest, Me, MemberRole, Membership, Message, MessageCreateRequest, MessageType, NetworkError, PoolseConfig, PoolseError, PoolseUserProfile, RateLimitedError, RealtimeStatus, Uuid } from '@poolse/sdk';
4
4
  import { ReactNode } from 'react';
5
5
 
6
6
  interface PoolseProviderProps {
@@ -325,38 +325,76 @@ declare function usePresence(conversationId: string): UsePresenceState;
325
325
  */
326
326
  declare function useRealtimeStatus(): RealtimeStatus;
327
327
 
328
+ type UploadItemStatus = 'pending' | 'uploading' | 'ready' | 'error' | 'cancelled';
329
+ interface UploadItem {
330
+ /** Local-only id (random per-enqueue). Stable for keys + `cancel(localId)` / `remove(localId)`. */
331
+ localId: string;
332
+ filename: string;
333
+ contentType: string;
334
+ byteSize: number;
335
+ status: UploadItemStatus;
336
+ /** Bytes uploaded so far; 0 until the PUT actually begins. */
337
+ loaded: number;
338
+ /** Resolved attachment row once `status === 'ready'`. Null otherwise. */
339
+ attachment: Attachment | null;
340
+ /** Set when `status === 'error'`. */
341
+ error: Error | null;
342
+ }
328
343
  interface UseAttachmentUploadState {
329
- /** Last successfully uploaded attachment, or null. */
344
+ /** Every enqueued item, in enqueue order, until removed by `remove()` / `reset()`. */
345
+ queue: UploadItem[];
346
+ /**
347
+ * Last attachment that reached `ready`, for the common one-file
348
+ * "did the upload finish" check. Null after `reset()` / `remove()`
349
+ * of the most recent ready item. Multi-file callers should iterate
350
+ * `queue` instead.
351
+ */
330
352
  attachment: Attachment | null;
353
+ /** True while at least one item is `pending` or `uploading`. */
331
354
  uploading: boolean;
355
+ /** Most recent item-level error, or null. */
332
356
  error: Error | null;
333
357
  /**
334
- * Run the upload pipeline (presigned-URL request + PUT). Updates
335
- * local state with the resolved attachment on success; sets `error`
336
- * and rethrows on failure so callers can surface a toast.
358
+ * Enqueue a single upload. Resolves with the final attachment row.
359
+ * Rejects with the underlying error (item's `status` becomes
360
+ * `'error'`); aborts surface as a DOMException('AbortError') and the
361
+ * item's status becomes `'cancelled'`.
337
362
  */
338
363
  upload: (input: AttachmentUploadInput) => Promise<Attachment>;
339
- /** Reset state (clears `attachment`, `error`). */
364
+ /**
365
+ * Enqueue many at once — drag-and-drop, multi-file picker. Items run
366
+ * in parallel (the SDK is per-call independent). Resolves with the
367
+ * attachments in the same order as the input; if any item fails the
368
+ * whole promise rejects after the others settle.
369
+ */
370
+ uploadAll: (inputs: AttachmentUploadInput[]) => Promise<Attachment[]>;
371
+ /** Abort an in-flight upload. Sets the item's status to `cancelled`. No-op if not in flight. */
372
+ cancel: (localId: string) => void;
373
+ /** Drop an item from the queue. Cancels first if it's still in flight. */
374
+ remove: (localId: string) => void;
375
+ /** Drop everything + abort any in-flight uploads. */
340
376
  reset: () => void;
341
377
  }
342
378
  /**
343
- * Stateful wrapper around `chat.attachments.upload`. Provides
344
- * `uploading` + `error` flags for the common "show a spinner while
345
- * the PUT is in flight" UI, plus retention of the last attachment
346
- * so the caller can reference its id when sending a message.
379
+ * Stateful wrapper around `chat.attachments.upload`. Exposes a queue
380
+ * of in-flight / completed uploads with per-item progress + per-item
381
+ * cancel — drag-and-drop, multi-file picker, retry chips, all without
382
+ * the host app reimplementing AbortController plumbing.
347
383
  *
348
384
  * ```tsx
349
- * const { upload, uploading, attachment } = useAttachmentUpload();
350
- * const onPick = async (file: File) => {
351
- * const att = await upload({
352
- * body: file, contentType: file.type, byteSize: file.size, filename: file.name
353
- * });
385
+ * const { upload, queue, cancel, remove } = useAttachmentUpload();
386
+ * const onPick = async (files: File[]) => {
387
+ * const atts = await uploadAll(files.map(f => ({
388
+ * body: f, contentType: f.type, byteSize: f.size, filename: f.name
389
+ * })));
354
390
  * await chat.conversations.one(convId).messages.send({
355
- * body: 'attached',
356
- * custom_data: { attachment_id: att.id },
391
+ * body: '', attachment_ids: atts.map(a => a.id),
357
392
  * });
358
393
  * };
359
394
  * ```
395
+ *
396
+ * The legacy `upload(input)` → `Promise<Attachment>` shape is preserved
397
+ * so older callers keep working — they just won't render the queue UI.
360
398
  */
361
399
  declare function useAttachmentUpload(): UseAttachmentUploadState;
362
400
 
@@ -383,4 +421,23 @@ interface UseAttachmentUrlState {
383
421
  */
384
422
  declare function useAttachmentUrl(attachmentId: Uuid | null): UseAttachmentUrlState;
385
423
 
386
- export { PoolseProvider, type PoolseProviderProps, type ReactionMap, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping };
424
+ interface UseUserState {
425
+ /** Resolved profile, or null when the resolver returned null / errored / isn't configured. */
426
+ profile: PoolseUserProfile | null;
427
+ /** True while the SDK is mid-fetch (or before the first fetch fires). */
428
+ loading: boolean;
429
+ }
430
+ /**
431
+ * Resolve a poolse user_id to the customer's display name + avatar
432
+ * URL via the configured `userResolver`. Returns `{ profile: null,
433
+ * loading: false }` when:
434
+ * * no resolver is configured (customer hasn't wired one up)
435
+ * * resolver returned null (user not found)
436
+ * * resolver threw (logged once, then cached as null)
437
+ *
438
+ * Pass `null` / empty string to opt out (the hook becomes a no-op).
439
+ * Useful for self bubbles (you already know who you are).
440
+ */
441
+ declare function useUser(userId: Uuid | null | undefined): UseUserState;
442
+
443
+ 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 };
package/dist/index.js CHANGED
@@ -31,7 +31,11 @@ function PoolseProvider({ config, children }) {
31
31
  ...initial.generateIdempotencyKey ? { generateIdempotencyKey: () => configRef.current.generateIdempotencyKey() } : {},
32
32
  ...initial.wsUrl !== void 0 ? { wsUrl: initial.wsUrl } : {},
33
33
  ...initial.socketPath !== void 0 ? { socketPath: initial.socketPath } : {},
34
- ...initial.onSocketError ? { onSocketError: (err) => configRef.current.onSocketError(err) } : {}
34
+ ...initial.onSocketError ? { onSocketError: (err) => configRef.current.onSocketError(err) } : {},
35
+ // Customer's user metadata resolver — name + avatar lookup.
36
+ // Read through the ref so the customer can swap the closure
37
+ // (e.g. switch tenant context) without re-mounting.
38
+ ...initial.userResolver ? { userResolver: (id) => configRef.current.userResolver(id) } : {}
35
39
  });
36
40
  }, []);
37
41
  if (process.env.NODE_ENV !== "production") {
@@ -223,7 +227,6 @@ function useMembers(conversationId) {
223
227
  hasFetchedRef.current = true;
224
228
  } catch (err) {
225
229
  if (err?.name === "AbortError") return;
226
- console.error("[poolse] useMembers fetch failed:", err);
227
230
  setError(err instanceof Error ? err : new Error(String(err)));
228
231
  } finally {
229
232
  setLoading(false);
@@ -242,7 +245,6 @@ function useMembers(conversationId) {
242
245
  if (!conversationId) return;
243
246
  const channel = chat.realtime.conversation(conversationId);
244
247
  const off = channel.onMemberRead((evt) => {
245
- console.log("[poolse] member:read received", evt);
246
248
  if (!hasFetchedRef.current) {
247
249
  pendingReadsRef.current.push(evt);
248
250
  return;
@@ -782,45 +784,107 @@ function useRealtimeStatus() {
782
784
  }
783
785
  function useAttachmentUpload() {
784
786
  const chat = usePoolse();
785
- const [attachment, setAttachment] = useState(null);
786
- const [uploading, setUploading] = useState(false);
787
- const [error, setError] = useState(null);
788
- const inFlightRef = useRef(null);
789
- const upload = useCallback(
790
- async (input) => {
791
- inFlightRef.current?.abort();
787
+ const [queue, setQueue] = useState([]);
788
+ const inFlightRef = useRef(/* @__PURE__ */ new Map());
789
+ const mountedRef = useRef(true);
790
+ useEffect(() => {
791
+ mountedRef.current = true;
792
+ return () => {
793
+ mountedRef.current = false;
794
+ for (const v of inFlightRef.current.values()) v.controller.abort();
795
+ inFlightRef.current.clear();
796
+ };
797
+ }, []);
798
+ const updateItem = useCallback((localId, patch) => {
799
+ if (!mountedRef.current) return;
800
+ setQueue((q) => q.map((it) => it.localId === localId ? { ...it, ...patch } : it));
801
+ }, []);
802
+ const runUpload = useCallback(
803
+ async (localId, input) => {
792
804
  const controller = new AbortController();
793
- inFlightRef.current = controller;
794
- setUploading(true);
795
- setError(null);
805
+ inFlightRef.current.set(localId, { controller });
806
+ updateItem(localId, { status: "uploading", loaded: 0 });
796
807
  try {
797
- const att = await chat.attachments.upload(input, { signal: controller.signal });
798
- if (inFlightRef.current === controller) {
799
- setAttachment(att);
800
- }
808
+ const att = await chat.attachments.upload(input, {
809
+ signal: controller.signal,
810
+ onProgress: (e) => updateItem(localId, { loaded: e.loaded })
811
+ });
812
+ updateItem(localId, {
813
+ status: "ready",
814
+ loaded: input.byteSize,
815
+ attachment: att,
816
+ error: null
817
+ });
801
818
  return att;
802
819
  } catch (err) {
803
- if (err?.name === "AbortError") throw err;
820
+ if (err?.name === "AbortError") {
821
+ updateItem(localId, { status: "cancelled" });
822
+ throw err;
823
+ }
804
824
  const e = err instanceof Error ? err : new Error(String(err));
805
- setError(e);
825
+ updateItem(localId, { status: "error", error: e });
806
826
  throw e;
807
827
  } finally {
808
- if (inFlightRef.current === controller) {
809
- setUploading(false);
810
- inFlightRef.current = null;
811
- }
828
+ inFlightRef.current.delete(localId);
812
829
  }
813
830
  },
814
- [chat]
831
+ [chat, updateItem]
832
+ );
833
+ const enqueue = useCallback((input) => {
834
+ const localId = generateLocalId();
835
+ const item = {
836
+ localId,
837
+ filename: input.filename ?? "file",
838
+ contentType: input.contentType,
839
+ byteSize: input.byteSize,
840
+ status: "pending",
841
+ loaded: 0,
842
+ attachment: null,
843
+ error: null
844
+ };
845
+ setQueue((q) => [...q, item]);
846
+ return item;
847
+ }, []);
848
+ const upload = useCallback(
849
+ (input) => {
850
+ const item = enqueue(input);
851
+ return runUpload(item.localId, input);
852
+ },
853
+ [enqueue, runUpload]
854
+ );
855
+ const uploadAll = useCallback(
856
+ (inputs) => {
857
+ const items = inputs.map((inp) => ({ item: enqueue(inp), input: inp }));
858
+ return Promise.all(items.map(({ item, input }) => runUpload(item.localId, input)));
859
+ },
860
+ [enqueue, runUpload]
861
+ );
862
+ const cancel = useCallback((localId) => {
863
+ const inf = inFlightRef.current.get(localId);
864
+ if (inf) inf.controller.abort();
865
+ }, []);
866
+ const remove = useCallback(
867
+ (localId) => {
868
+ const inf = inFlightRef.current.get(localId);
869
+ if (inf) inf.controller.abort();
870
+ setQueue((q) => q.filter((it) => it.localId !== localId));
871
+ },
872
+ []
815
873
  );
816
874
  const reset = useCallback(() => {
817
- inFlightRef.current?.abort();
818
- inFlightRef.current = null;
819
- setAttachment(null);
820
- setError(null);
821
- setUploading(false);
875
+ for (const v of inFlightRef.current.values()) v.controller.abort();
876
+ inFlightRef.current.clear();
877
+ setQueue([]);
822
878
  }, []);
823
- return { attachment, uploading, error, upload, reset };
879
+ const uploading = queue.some((it) => it.status === "pending" || it.status === "uploading");
880
+ const error = queue.reduce((acc, it) => it.error ?? acc, null);
881
+ const attachment = queue.reduce((acc, it) => it.attachment ?? acc, null);
882
+ return { queue, attachment, uploading, error, upload, uploadAll, cancel, remove, reset };
883
+ }
884
+ function generateLocalId() {
885
+ const c = globalThis.crypto;
886
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
887
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
824
888
  }
825
889
  function useAttachmentUrl(attachmentId) {
826
890
  const chat = usePoolse();
@@ -855,7 +919,36 @@ function useAttachmentUrl(attachmentId) {
855
919
  }, [chat, attachmentId]);
856
920
  return { url, loading, error };
857
921
  }
922
+ var EMPTY = { profile: null, loading: false };
923
+ function useUser(userId) {
924
+ const chat = usePoolse();
925
+ const [state, setState] = useState(() => {
926
+ if (!userId) return EMPTY;
927
+ const cached = chat.users.peek(userId);
928
+ if (cached === void 0) return { profile: null, loading: true };
929
+ return { profile: cached, loading: false };
930
+ });
931
+ useEffect(() => {
932
+ if (!userId) {
933
+ setState(EMPTY);
934
+ return;
935
+ }
936
+ const apply = () => {
937
+ const cached = chat.users.peek(userId);
938
+ if (cached === void 0) {
939
+ setState({ profile: null, loading: true });
940
+ void chat.users.get(userId);
941
+ } else {
942
+ setState({ profile: cached, loading: false });
943
+ }
944
+ };
945
+ const off = chat.users.subscribe(userId, apply);
946
+ apply();
947
+ return off;
948
+ }, [chat, userId]);
949
+ return state;
950
+ }
858
951
 
859
- export { PoolseProvider, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping };
952
+ export { PoolseProvider, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
860
953
  //# sourceMappingURL=index.js.map
861
954
  //# sourceMappingURL=index.js.map