@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/CHANGELOG.md +9 -0
- package/README.md +37 -15
- package/dist/index.cjs +124 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -19
- package/dist/index.d.ts +76 -19
- package/dist/index.js +124 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `@poolse/react` are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[semver](https://semver.org).
|
|
6
6
|
|
|
7
|
+
## [1.0.0-beta.0] — 2026-06-01
|
|
8
|
+
|
|
9
|
+
First beta of the stable API. Promotes the full 0.2.x hook surface
|
|
10
|
+
(useMessages with reply_count routing, useThread with full-thread
|
|
11
|
+
fetch, useMembers with buffered read-receipt events,
|
|
12
|
+
useConversations with unreadCounts + markConversationRead) to a
|
|
13
|
+
stable namespace. No breaking changes relative to `0.2.0-alpha.7`
|
|
14
|
+
— version bump + removed verbose diagnostic logs.
|
|
15
|
+
|
|
7
16
|
## [0.2.0] — 2026-06-01
|
|
8
17
|
|
|
9
18
|
### Fixed
|
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);
|
|
@@ -243,7 +246,6 @@ function useMembers(conversationId) {
|
|
|
243
246
|
if (!conversationId) return;
|
|
244
247
|
const channel = chat.realtime.conversation(conversationId);
|
|
245
248
|
const off = channel.onMemberRead((evt) => {
|
|
246
|
-
console.log("[poolse] member:read received", evt);
|
|
247
249
|
if (!hasFetchedRef.current) {
|
|
248
250
|
pendingReadsRef.current.push(evt);
|
|
249
251
|
return;
|
|
@@ -783,45 +785,107 @@ function useRealtimeStatus() {
|
|
|
783
785
|
}
|
|
784
786
|
function useAttachmentUpload() {
|
|
785
787
|
const chat = usePoolse();
|
|
786
|
-
const [
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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) => {
|
|
793
805
|
const controller = new AbortController();
|
|
794
|
-
inFlightRef.current
|
|
795
|
-
|
|
796
|
-
setError(null);
|
|
806
|
+
inFlightRef.current.set(localId, { controller });
|
|
807
|
+
updateItem(localId, { status: "uploading", loaded: 0 });
|
|
797
808
|
try {
|
|
798
|
-
const att = await chat.attachments.upload(input, {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
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
|
+
});
|
|
802
819
|
return att;
|
|
803
820
|
} catch (err) {
|
|
804
|
-
if (err?.name === "AbortError")
|
|
821
|
+
if (err?.name === "AbortError") {
|
|
822
|
+
updateItem(localId, { status: "cancelled" });
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
805
825
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
806
|
-
|
|
826
|
+
updateItem(localId, { status: "error", error: e });
|
|
807
827
|
throw e;
|
|
808
828
|
} finally {
|
|
809
|
-
|
|
810
|
-
setUploading(false);
|
|
811
|
-
inFlightRef.current = null;
|
|
812
|
-
}
|
|
829
|
+
inFlightRef.current.delete(localId);
|
|
813
830
|
}
|
|
814
831
|
},
|
|
815
|
-
[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
|
+
[]
|
|
816
874
|
);
|
|
817
875
|
const reset = react.useCallback(() => {
|
|
818
|
-
inFlightRef.current
|
|
819
|
-
inFlightRef.current
|
|
820
|
-
|
|
821
|
-
setError(null);
|
|
822
|
-
setUploading(false);
|
|
876
|
+
for (const v of inFlightRef.current.values()) v.controller.abort();
|
|
877
|
+
inFlightRef.current.clear();
|
|
878
|
+
setQueue([]);
|
|
823
879
|
}, []);
|
|
824
|
-
|
|
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)}`;
|
|
825
889
|
}
|
|
826
890
|
function useAttachmentUrl(attachmentId) {
|
|
827
891
|
const chat = usePoolse();
|
|
@@ -856,6 +920,35 @@ function useAttachmentUrl(attachmentId) {
|
|
|
856
920
|
}, [chat, attachmentId]);
|
|
857
921
|
return { url, loading, error };
|
|
858
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
|
+
}
|
|
859
952
|
|
|
860
953
|
Object.defineProperty(exports, "ApiError", {
|
|
861
954
|
enumerable: true,
|
|
@@ -891,5 +984,6 @@ exports.useReactions = useReactions;
|
|
|
891
984
|
exports.useRealtimeStatus = useRealtimeStatus;
|
|
892
985
|
exports.useThread = useThread;
|
|
893
986
|
exports.useTyping = useTyping;
|
|
987
|
+
exports.useUser = useUser;
|
|
894
988
|
//# sourceMappingURL=index.cjs.map
|
|
895
989
|
//# sourceMappingURL=index.cjs.map
|