@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/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()` |
|
|
64
|
-
| `useAttachmentUrl(id)` | Presigned download URL
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
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);
|
|
@@ -750,6 +753,7 @@ function usePresence(conversationId) {
|
|
|
750
753
|
const [online, setOnline] = react.useState(/* @__PURE__ */ new Set());
|
|
751
754
|
react.useEffect(() => {
|
|
752
755
|
setOnline(/* @__PURE__ */ new Set());
|
|
756
|
+
if (!conversationId) return;
|
|
753
757
|
const conv = chat.realtime.conversation(conversationId);
|
|
754
758
|
const applyState = (state) => {
|
|
755
759
|
setOnline(new Set(Object.keys(state)));
|
|
@@ -782,45 +786,107 @@ function useRealtimeStatus() {
|
|
|
782
786
|
}
|
|
783
787
|
function useAttachmentUpload() {
|
|
784
788
|
const chat = usePoolse();
|
|
785
|
-
const [
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
789
|
+
const [queue, setQueue] = react.useState([]);
|
|
790
|
+
const inFlightRef = react.useRef(/* @__PURE__ */ new Map());
|
|
791
|
+
const mountedRef = react.useRef(true);
|
|
792
|
+
react.useEffect(() => {
|
|
793
|
+
mountedRef.current = true;
|
|
794
|
+
return () => {
|
|
795
|
+
mountedRef.current = false;
|
|
796
|
+
for (const v of inFlightRef.current.values()) v.controller.abort();
|
|
797
|
+
inFlightRef.current.clear();
|
|
798
|
+
};
|
|
799
|
+
}, []);
|
|
800
|
+
const updateItem = react.useCallback((localId, patch) => {
|
|
801
|
+
if (!mountedRef.current) return;
|
|
802
|
+
setQueue((q) => q.map((it) => it.localId === localId ? { ...it, ...patch } : it));
|
|
803
|
+
}, []);
|
|
804
|
+
const runUpload = react.useCallback(
|
|
805
|
+
async (localId, input) => {
|
|
792
806
|
const controller = new AbortController();
|
|
793
|
-
inFlightRef.current
|
|
794
|
-
|
|
795
|
-
setError(null);
|
|
807
|
+
inFlightRef.current.set(localId, { controller });
|
|
808
|
+
updateItem(localId, { status: "uploading", loaded: 0 });
|
|
796
809
|
try {
|
|
797
|
-
const att = await chat.attachments.upload(input, {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
810
|
+
const att = await chat.attachments.upload(input, {
|
|
811
|
+
signal: controller.signal,
|
|
812
|
+
onProgress: (e) => updateItem(localId, { loaded: e.loaded })
|
|
813
|
+
});
|
|
814
|
+
updateItem(localId, {
|
|
815
|
+
status: "ready",
|
|
816
|
+
loaded: input.byteSize,
|
|
817
|
+
attachment: att,
|
|
818
|
+
error: null
|
|
819
|
+
});
|
|
801
820
|
return att;
|
|
802
821
|
} catch (err) {
|
|
803
|
-
if (err?.name === "AbortError")
|
|
822
|
+
if (err?.name === "AbortError") {
|
|
823
|
+
updateItem(localId, { status: "cancelled" });
|
|
824
|
+
throw err;
|
|
825
|
+
}
|
|
804
826
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
805
|
-
|
|
827
|
+
updateItem(localId, { status: "error", error: e });
|
|
806
828
|
throw e;
|
|
807
829
|
} finally {
|
|
808
|
-
|
|
809
|
-
setUploading(false);
|
|
810
|
-
inFlightRef.current = null;
|
|
811
|
-
}
|
|
830
|
+
inFlightRef.current.delete(localId);
|
|
812
831
|
}
|
|
813
832
|
},
|
|
814
|
-
[chat]
|
|
833
|
+
[chat, updateItem]
|
|
834
|
+
);
|
|
835
|
+
const enqueue = react.useCallback((input) => {
|
|
836
|
+
const localId = generateLocalId();
|
|
837
|
+
const item = {
|
|
838
|
+
localId,
|
|
839
|
+
filename: input.filename ?? "file",
|
|
840
|
+
contentType: input.contentType,
|
|
841
|
+
byteSize: input.byteSize,
|
|
842
|
+
status: "pending",
|
|
843
|
+
loaded: 0,
|
|
844
|
+
attachment: null,
|
|
845
|
+
error: null
|
|
846
|
+
};
|
|
847
|
+
setQueue((q) => [...q, item]);
|
|
848
|
+
return item;
|
|
849
|
+
}, []);
|
|
850
|
+
const upload = react.useCallback(
|
|
851
|
+
(input) => {
|
|
852
|
+
const item = enqueue(input);
|
|
853
|
+
return runUpload(item.localId, input);
|
|
854
|
+
},
|
|
855
|
+
[enqueue, runUpload]
|
|
856
|
+
);
|
|
857
|
+
const uploadAll = react.useCallback(
|
|
858
|
+
(inputs) => {
|
|
859
|
+
const items = inputs.map((inp) => ({ item: enqueue(inp), input: inp }));
|
|
860
|
+
return Promise.all(items.map(({ item, input }) => runUpload(item.localId, input)));
|
|
861
|
+
},
|
|
862
|
+
[enqueue, runUpload]
|
|
863
|
+
);
|
|
864
|
+
const cancel = react.useCallback((localId) => {
|
|
865
|
+
const inf = inFlightRef.current.get(localId);
|
|
866
|
+
if (inf) inf.controller.abort();
|
|
867
|
+
}, []);
|
|
868
|
+
const remove = react.useCallback(
|
|
869
|
+
(localId) => {
|
|
870
|
+
const inf = inFlightRef.current.get(localId);
|
|
871
|
+
if (inf) inf.controller.abort();
|
|
872
|
+
setQueue((q) => q.filter((it) => it.localId !== localId));
|
|
873
|
+
},
|
|
874
|
+
[]
|
|
815
875
|
);
|
|
816
876
|
const reset = react.useCallback(() => {
|
|
817
|
-
inFlightRef.current
|
|
818
|
-
inFlightRef.current
|
|
819
|
-
|
|
820
|
-
setError(null);
|
|
821
|
-
setUploading(false);
|
|
877
|
+
for (const v of inFlightRef.current.values()) v.controller.abort();
|
|
878
|
+
inFlightRef.current.clear();
|
|
879
|
+
setQueue([]);
|
|
822
880
|
}, []);
|
|
823
|
-
|
|
881
|
+
const uploading = queue.some((it) => it.status === "pending" || it.status === "uploading");
|
|
882
|
+
const error = queue.reduce((acc, it) => it.error ?? acc, null);
|
|
883
|
+
const attachment = queue.reduce((acc, it) => it.attachment ?? acc, null);
|
|
884
|
+
return { queue, attachment, uploading, error, upload, uploadAll, cancel, remove, reset };
|
|
885
|
+
}
|
|
886
|
+
function generateLocalId() {
|
|
887
|
+
const c = globalThis.crypto;
|
|
888
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
889
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
824
890
|
}
|
|
825
891
|
function useAttachmentUrl(attachmentId) {
|
|
826
892
|
const chat = usePoolse();
|
|
@@ -873,12 +939,12 @@ function useUser(userId) {
|
|
|
873
939
|
const cached = chat.users.peek(userId);
|
|
874
940
|
if (cached === void 0) {
|
|
875
941
|
setState({ profile: null, loading: true });
|
|
942
|
+
void chat.users.get(userId);
|
|
876
943
|
} else {
|
|
877
944
|
setState({ profile: cached, loading: false });
|
|
878
945
|
}
|
|
879
946
|
};
|
|
880
947
|
const off = chat.users.subscribe(userId, apply);
|
|
881
|
-
void chat.users.get(userId);
|
|
882
948
|
apply();
|
|
883
949
|
return off;
|
|
884
950
|
}, [chat, userId]);
|