@poolse/react 1.0.1-beta.0 → 1.0.2
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 +37 -15
- package/dist/index.cjs +96 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -17
- package/dist/index.d.ts +55 -17
- package/dist/index.js +96 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
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
|
-
/**
|
|
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`.
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
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,
|
|
350
|
-
* const onPick = async (
|
|
351
|
-
* const
|
|
352
|
-
* body:
|
|
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: '
|
|
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
|
|
|
@@ -402,4 +440,4 @@ interface UseUserState {
|
|
|
402
440
|
*/
|
|
403
441
|
declare function useUser(userId: Uuid | null | undefined): UseUserState;
|
|
404
442
|
|
|
405
|
-
export { PoolseProvider, type PoolseProviderProps, type ReactionMap, type UseUserState, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
|
|
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);
|
|
@@ -749,6 +752,7 @@ function usePresence(conversationId) {
|
|
|
749
752
|
const [online, setOnline] = useState(/* @__PURE__ */ new Set());
|
|
750
753
|
useEffect(() => {
|
|
751
754
|
setOnline(/* @__PURE__ */ new Set());
|
|
755
|
+
if (!conversationId) return;
|
|
752
756
|
const conv = chat.realtime.conversation(conversationId);
|
|
753
757
|
const applyState = (state) => {
|
|
754
758
|
setOnline(new Set(Object.keys(state)));
|
|
@@ -781,45 +785,107 @@ function useRealtimeStatus() {
|
|
|
781
785
|
}
|
|
782
786
|
function useAttachmentUpload() {
|
|
783
787
|
const chat = usePoolse();
|
|
784
|
-
const [
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
788
|
+
const [queue, setQueue] = useState([]);
|
|
789
|
+
const inFlightRef = useRef(/* @__PURE__ */ new Map());
|
|
790
|
+
const mountedRef = useRef(true);
|
|
791
|
+
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 = useCallback((localId, patch) => {
|
|
800
|
+
if (!mountedRef.current) return;
|
|
801
|
+
setQueue((q) => q.map((it) => it.localId === localId ? { ...it, ...patch } : it));
|
|
802
|
+
}, []);
|
|
803
|
+
const runUpload = useCallback(
|
|
804
|
+
async (localId, input) => {
|
|
791
805
|
const controller = new AbortController();
|
|
792
|
-
inFlightRef.current
|
|
793
|
-
|
|
794
|
-
setError(null);
|
|
806
|
+
inFlightRef.current.set(localId, { controller });
|
|
807
|
+
updateItem(localId, { status: "uploading", loaded: 0 });
|
|
795
808
|
try {
|
|
796
|
-
const att = await chat.attachments.upload(input, {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
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
|
+
});
|
|
800
819
|
return att;
|
|
801
820
|
} catch (err) {
|
|
802
|
-
if (err?.name === "AbortError")
|
|
821
|
+
if (err?.name === "AbortError") {
|
|
822
|
+
updateItem(localId, { status: "cancelled" });
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
803
825
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
804
|
-
|
|
826
|
+
updateItem(localId, { status: "error", error: e });
|
|
805
827
|
throw e;
|
|
806
828
|
} finally {
|
|
807
|
-
|
|
808
|
-
setUploading(false);
|
|
809
|
-
inFlightRef.current = null;
|
|
810
|
-
}
|
|
829
|
+
inFlightRef.current.delete(localId);
|
|
811
830
|
}
|
|
812
831
|
},
|
|
813
|
-
[chat]
|
|
832
|
+
[chat, updateItem]
|
|
833
|
+
);
|
|
834
|
+
const enqueue = 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 = useCallback(
|
|
850
|
+
(input) => {
|
|
851
|
+
const item = enqueue(input);
|
|
852
|
+
return runUpload(item.localId, input);
|
|
853
|
+
},
|
|
854
|
+
[enqueue, runUpload]
|
|
855
|
+
);
|
|
856
|
+
const uploadAll = 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 = useCallback((localId) => {
|
|
864
|
+
const inf = inFlightRef.current.get(localId);
|
|
865
|
+
if (inf) inf.controller.abort();
|
|
866
|
+
}, []);
|
|
867
|
+
const remove = 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
|
+
[]
|
|
814
874
|
);
|
|
815
875
|
const reset = useCallback(() => {
|
|
816
|
-
inFlightRef.current
|
|
817
|
-
inFlightRef.current
|
|
818
|
-
|
|
819
|
-
setError(null);
|
|
820
|
-
setUploading(false);
|
|
876
|
+
for (const v of inFlightRef.current.values()) v.controller.abort();
|
|
877
|
+
inFlightRef.current.clear();
|
|
878
|
+
setQueue([]);
|
|
821
879
|
}, []);
|
|
822
|
-
|
|
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)}`;
|
|
823
889
|
}
|
|
824
890
|
function useAttachmentUrl(attachmentId) {
|
|
825
891
|
const chat = usePoolse();
|
|
@@ -872,12 +938,12 @@ function useUser(userId) {
|
|
|
872
938
|
const cached = chat.users.peek(userId);
|
|
873
939
|
if (cached === void 0) {
|
|
874
940
|
setState({ profile: null, loading: true });
|
|
941
|
+
void chat.users.get(userId);
|
|
875
942
|
} else {
|
|
876
943
|
setState({ profile: cached, loading: false });
|
|
877
944
|
}
|
|
878
945
|
};
|
|
879
946
|
const off = chat.users.subscribe(userId, apply);
|
|
880
|
-
void chat.users.get(userId);
|
|
881
947
|
apply();
|
|
882
948
|
return off;
|
|
883
949
|
}, [chat, userId]);
|