@poolse/react 2.29.0 → 3.1.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 +22 -0
- package/LICENSE +206 -21
- package/README.md +71 -293
- package/dist/index.cjs +33 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# `@poolse/react`
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
React provider and hooks for [poolse](https://poolse.dev), on top of
|
|
4
|
+
[`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk). No UI. The same
|
|
5
|
+
hooks work on the web and in React Native. For components, see
|
|
6
|
+
[`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui) (web)
|
|
7
|
+
or [`@poolse/react-native`](https://www.npmjs.com/package/@poolse/react-native).
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
> **⚠️ Upgrading from 1.x?** See [MIGRATING.md](https://github.com/poolse-hq/js-sdk/blob/main/MIGRATING.md). `useUser`, `useTyping`, `usePresence`, and `useMembers().removeMember` all key by your `external_id` now instead of poolse uuids.
|
|
9
|
+
Full reference: <https://poolse.dev/docs/sdk/react>
|
|
8
10
|
|
|
9
11
|
## Install
|
|
10
12
|
|
|
@@ -12,34 +14,37 @@ React hooks library for realtime chat. Wraps [`@poolse/sdk`](https://www.npmjs.c
|
|
|
12
14
|
npm install @poolse/react @poolse/sdk
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
`react
|
|
17
|
+
Peer: `react` ≥ 18. `@poolse/sdk` is a dependency; keep the same major
|
|
18
|
+
version if you also install it.
|
|
16
19
|
|
|
17
20
|
## Quick start
|
|
18
21
|
|
|
19
22
|
```tsx
|
|
20
|
-
import { PoolseProvider, useMessages } from '@poolse/react';
|
|
23
|
+
import { PoolseProvider, useMessages, type PoolseConfig } from '@poolse/react';
|
|
24
|
+
|
|
25
|
+
const config: PoolseConfig = {
|
|
26
|
+
getToken: async () => {
|
|
27
|
+
const res = await fetch('/api/chat-token', { method: 'POST' });
|
|
28
|
+
const { token } = await res.json();
|
|
29
|
+
return token;
|
|
30
|
+
},
|
|
31
|
+
userResolver: async (externalId) => {
|
|
32
|
+
const u = await fetch(`/api/users/${externalId}`).then((r) => r.json());
|
|
33
|
+
return { displayName: u.name, avatarUrl: u.avatar_url };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
21
36
|
|
|
22
|
-
function App() {
|
|
37
|
+
export function App() {
|
|
23
38
|
return (
|
|
24
|
-
<PoolseProvider
|
|
25
|
-
|
|
26
|
-
apiUrl: 'https://api.poolse.dev',
|
|
27
|
-
getToken: async () => {
|
|
28
|
-
const res = await fetch('/api/chat-token', { method: 'POST' });
|
|
29
|
-
const { token } = await res.json();
|
|
30
|
-
return token;
|
|
31
|
-
},
|
|
32
|
-
}}
|
|
33
|
-
>
|
|
34
|
-
<Chat conversationId="00000000-0000-0000-0000-000000000000" />
|
|
39
|
+
<PoolseProvider config={config}>
|
|
40
|
+
<Chat conversationId="…" />
|
|
35
41
|
</PoolseProvider>
|
|
36
42
|
);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function Chat({ conversationId }: { conversationId: string }) {
|
|
40
46
|
const { messages, send, loading } = useMessages(conversationId);
|
|
41
|
-
|
|
42
|
-
if (loading) return <div>Loading…</div>;
|
|
47
|
+
if (loading) return <p>Loading…</p>;
|
|
43
48
|
return (
|
|
44
49
|
<ul>
|
|
45
50
|
{messages.map((m) => (
|
|
@@ -51,285 +56,58 @@ function Chat({ conversationId }: { conversationId: string }) {
|
|
|
51
56
|
}
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<PoolseProvider config={config}>{children}</PoolseProvider>
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`config` is the same `PoolseConfig` the bare SDK accepts (see [`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk) for the full reference).
|
|
61
|
-
|
|
62
|
-
The provider builds **one** `Poolse` instance per mount. Connection-shaped fields (`apiUrl`, `wsUrl`, `socketPath`) are captured at construction and don't react to subsequent changes; the development build warns when you change them after mount. Function-valued fields (`getToken`, `fetch`, `userResolver`, `generateIdempotencyKey`, `onSocketError`) are read from a ref on every call, so passing fresh closures on every render is safe and expected:
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
// Fine — getToken is re-read through a ref each call.
|
|
66
|
-
<PoolseProvider config={{ apiUrl, getToken: () => session.token }}>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
To force a fresh client (sign-out, tenant swap, environment switch), remount the provider with a new `key`. On unmount, `chat.destroy()` runs, which closes the WebSocket and drops every joined channel.
|
|
70
|
-
|
|
71
|
-
### `usePoolse()`
|
|
72
|
-
|
|
73
|
-
Escape hatch — returns the underlying `Poolse` instance for callers that need direct REST or realtime access. Throws if there's no provider above it.
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
const chat = usePoolse();
|
|
77
|
-
const me = await chat.me.show();
|
|
78
|
-
```
|
|
59
|
+
The provider creates one `Poolse` instance per mount. Function options are
|
|
60
|
+
read through a ref, so new closures on each render are fine. Remount with a
|
|
61
|
+
new `key` to switch user.
|
|
79
62
|
|
|
80
63
|
## Hooks
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
markReadUpTo, // (messageId: Uuid) => Promise<void>
|
|
122
|
-
} = useMessages(conversationId);
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Optimistic sends with id-based dedup.** `send()` generates a UUID client-side, appends an optimistic row sorted to the very end (`sequence: MAX_SAFE_INTEGER`), then POSTs with that same id. Whichever lands first — the REST response or the realtime `message:new` echo — wins via `upsertById`; the other arrival is a no-op replace. Errors filter the temp row by id.
|
|
126
|
-
|
|
127
|
-
**Thread routing.** Realtime `message:new` events with `thread_root_id` set are **not** added to `messages` (replies live in the thread side-pane). Instead the hook finds the root locally and increments its `reply_count`. The REST list excludes replies too (`WHERE thread_root_id IS NULL`), so a refresh and a realtime burst stay consistent.
|
|
128
|
-
|
|
129
|
-
**Edits and deletes.** Both are optimistic with synchronous rollback on error. The hook captures a snapshot before mutating so the rollback isn't racing React's update queue.
|
|
130
|
-
|
|
131
|
-
**Read cursor.** `markReadUpTo(messageId)` fires the read against `POST /v1/conversations/:id/read`. The server broadcasts `member:read` to other clients; this hook itself doesn't mutate state (the `useMembers` hook does — see below).
|
|
132
|
-
|
|
133
|
-
### `useThread(conversationId, rootMessageId): { replies, sendReply, edit, delete, loadMore, hasMore, ... }`
|
|
134
|
-
|
|
135
|
-
Side-pane replies for one root message. The wire returns replies oldest-first; the hook keeps that order. The default fetch is 500 replies in one round-trip — most threads load whole. `loadMore()` paginates with an `after` cursor when a thread overflows.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const {
|
|
139
|
-
replies, // Message[] — oldest-first
|
|
140
|
-
loading,
|
|
141
|
-
error,
|
|
142
|
-
hasMore,
|
|
143
|
-
loadMore,
|
|
144
|
-
sendReply, // (attrs: Omit<MessageCreateRequest, 'reply_to_id'>) => Promise<Message>
|
|
145
|
-
edit,
|
|
146
|
-
delete: del,
|
|
147
|
-
} = useThread(conversationId, rootMessageId);
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Subscriptions filter by `thread_root_id === rootMessageId` so unrelated messages in the same conversation don't end up here. `sendReply` auto-fills `reply_to_id` with the root id — same optimistic-id dedup pattern as `useMessages.send`.
|
|
151
|
-
|
|
152
|
-
### `useReactions(messageId, opts): { reactions, addReaction, removeReaction }`
|
|
153
|
-
|
|
154
|
-
Live reaction map for one message. Subscribes to `reaction:added` / `reaction:removed` filtered to the message id.
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
const { reactions, addReaction, removeReaction } = useReactions(messageId, {
|
|
158
|
-
conversationId,
|
|
159
|
-
initialReactions: msg.reactions, // seed from the Message you already have
|
|
160
|
-
currentUserId: meId, // enables optimistic add/remove
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`reactions` is a `Record<emoji, Uuid[]>` (matching `Message.reactions`). Adds + removes are optimistic when `currentUserId` is provided; the optimistic delta is rolled back on error.
|
|
165
|
-
|
|
166
|
-
### `useTyping(conversationId): { typing, signalTyping }`
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
const { typing, signalTyping } = useTyping(conversationId);
|
|
170
|
-
// typing: Set<Uuid> of users currently typing (your own id is filtered out)
|
|
171
|
-
// signalTyping: client-side debounced to ≤ 1 ping / 500ms
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Call `signalTyping` on every keystroke; the hook drops events that arrive within 500ms of the last one, and the server has its own rate limit per (user, conversation) on top.
|
|
175
|
-
|
|
176
|
-
### `usePresence(conversationId): { online }`
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
const { online } = usePresence(conversationId);
|
|
180
|
-
// online: Set<Uuid> of users currently joined to the channel
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Replays the initial `presence_state` snapshot on join, then applies `presence_diff` deltas. Pass an empty string id to opt out (the set stays empty).
|
|
184
|
-
|
|
185
|
-
### `useMembers(conversationId): { members, addMembers, removeMember, refetch, ... }`
|
|
186
|
-
|
|
187
|
-
Conversation roster. Initial fetch is `GET /v1/conversations/:id/members`. Subscribes to `member:read` on the channel and advances the matching membership's `last_read_message_id` and `last_read_at` in place — which is how `<MessageBubble>`'s read-receipt glyph (in `@poolse/react-ui`) flips from sent to read in real time.
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
const {
|
|
191
|
-
members, // Membership[]
|
|
192
|
-
loading,
|
|
193
|
-
error,
|
|
194
|
-
refetch,
|
|
195
|
-
addMembers, // (externalIds: string[], { role? }?) => Promise<Membership[]>
|
|
196
|
-
removeMember, // (userId: Uuid) => Promise<void>
|
|
197
|
-
} = useMembers(conversationId);
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
`addMembers` deduplicates by id (server returns an idempotent add). `removeMember` is optimistic with snapshot rollback.
|
|
201
|
-
|
|
202
|
-
Empty `conversationId` opts out of the fetch (returns `{ members: [], loading: false }`).
|
|
203
|
-
|
|
204
|
-
`member:read` events that arrive before the initial fetch lands are buffered and replayed after `setMembers` commits — without that, reads in the subscribe → fetch-response gap would silently no-op against an empty member list.
|
|
205
|
-
|
|
206
|
-
### `useAttachmentUpload(): { queue, upload, uploadAll, cancel, remove, reset, ... }`
|
|
207
|
-
|
|
208
|
-
Queued multi-file uploads with per-item progress and cancel.
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
const { queue, upload, uploadAll, cancel, remove, reset, uploading, error } = useAttachmentUpload();
|
|
212
|
-
|
|
213
|
-
// Single file
|
|
214
|
-
const att = await upload({
|
|
215
|
-
body: file,
|
|
216
|
-
contentType: file.type,
|
|
217
|
-
byteSize: file.size,
|
|
218
|
-
filename: file.name,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Batch (parallel PUTs, returned in input order)
|
|
222
|
-
const atts = await uploadAll(
|
|
223
|
-
files.map((f) => ({ body: f, contentType: f.type, byteSize: f.size, filename: f.name })),
|
|
224
|
-
);
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
Each `UploadItem` carries `{ localId, filename, contentType, byteSize, status, loaded, attachment, error }` with `status` cycling through `'pending' → 'uploading' → 'ready' | 'error' | 'cancelled'`. `loaded` is byte progress (browser-only — `fetch` doesn't expose upload progress, so the hook switches to XHR when `onProgress` is wired internally). Cancellation per item via `cancel(localId)`; full teardown via `reset()`.
|
|
228
|
-
|
|
229
|
-
The hook tracks a `mountedRef` so a fast unmount aborts all in-flight uploads instead of leaking promises.
|
|
230
|
-
|
|
231
|
-
### `useAttachmentUrl(attachmentId): { url, loading, error }`
|
|
232
|
-
|
|
233
|
-
Fetches a presigned GET URL for an attachment. Pass `null` to skip.
|
|
234
|
-
|
|
235
|
-
The URL has a ~1 hour TTL server-side. The hook doesn't auto-refresh — most UI lifetimes are shorter than the TTL anyway, and silently re-fetching mid-render would break image elements. If you have a long-lived dashboard, remount the hook (or wrap it in a parent that re-keys on a timer).
|
|
236
|
-
|
|
237
|
-
### `useUser(userId): { profile, loading }`
|
|
238
|
-
|
|
239
|
-
Resolves a poolse `user_id` to your customer-supplied display name + avatar via the `userResolver` you configured on the provider. Cached + deduplicated across the app.
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
const { profile, loading } = useUser(message.sender_id);
|
|
243
|
-
// profile: { displayName: string; avatarUrl: string | null } | null
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
If you don't configure `userResolver`, the hook always returns `{ profile: null, loading: false }`. Components in `@poolse/react-ui` (sender labels, mention dropdowns, member lists) fall back to a stable color-tinted initials avatar in that case.
|
|
247
|
-
|
|
248
|
-
Pass falsy `userId` to no-op the hook (used internally by self-message bubbles, which don't need to resolve the sender).
|
|
249
|
-
|
|
250
|
-
### `useRealtimeStatus(): RealtimeStatus`
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
const status = useRealtimeStatus();
|
|
254
|
-
// 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closed'
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
For "Reconnecting…" banners without coupling to socket internals.
|
|
258
|
-
|
|
259
|
-
## Wiring user identity
|
|
260
|
-
|
|
261
|
-
The SDK doesn't store names or avatars — that's your app's data. Wire `userResolver` once on the provider and every component that renders a sender lights up:
|
|
262
|
-
|
|
263
|
-
```tsx
|
|
264
|
-
<PoolseProvider
|
|
265
|
-
config={{
|
|
266
|
-
apiUrl,
|
|
267
|
-
getToken,
|
|
268
|
-
userResolver: async (poolseUserId) => {
|
|
269
|
-
// Translate poolse's user_id to your own user record.
|
|
270
|
-
const u = await fetch(`/api/users/by-poolse-id/${poolseUserId}`).then((r) => r.json());
|
|
271
|
-
return { displayName: u.full_name, avatarUrl: u.avatar_url };
|
|
272
|
-
},
|
|
273
|
-
}}
|
|
274
|
-
>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Resolved profiles are cached in-memory and concurrent lookups for the same id share one in-flight promise. Resolver errors are logged once (via `console.error` with `[poolse]` prefix) and cached as `null` so a single transient failure doesn't trigger a retry storm.
|
|
278
|
-
|
|
279
|
-
## What you can rely on
|
|
280
|
-
|
|
281
|
-
- **Mount-once SDK instance.** Stable WebSocket and channel subscriptions for the life of the provider; safe to pass fresh inline callbacks on every render.
|
|
282
|
-
- **Auto-rejoin on reconnect.** Phoenix handles channel rejoin internally; the JWT is re-read from `getToken` each handshake.
|
|
283
|
-
- **Optimistic dedup by client-generated id.** `useMessages.send` and `useThread.sendReply` insert under the same id the server will assign, so the realtime echo upserts in place rather than duplicating.
|
|
284
|
-
- **Synchronous rollback for optimistic edits.** Edit / delete / `removeReaction` / `removeMember` capture a ref snapshot before mutating, then restore the snapshot on error rather than relying on `setState((prev) => ...)`.
|
|
285
|
-
- **Buffered early events.** `useMembers` buffers `member:read` events that race the initial member fetch.
|
|
286
|
-
- **Abort-safe.** Hooks that fetch on mount cancel in-flight requests on unmount and ignore `AbortError`. Aborted fetches don't surface as "Failed to load" errors.
|
|
287
|
-
|
|
288
|
-
## Exports
|
|
289
|
-
|
|
290
|
-
```ts
|
|
291
|
-
// Provider
|
|
292
|
-
export { PoolseProvider, usePoolse, type PoolseProviderProps };
|
|
293
|
-
|
|
294
|
-
// Hooks
|
|
295
|
-
export { useMe, useConversation, useConversations, useMembers, useMessages };
|
|
296
|
-
export { useReactions, type ReactionMap };
|
|
297
|
-
export { useThread, useTyping, usePresence, useRealtimeStatus };
|
|
298
|
-
export { useAttachmentUpload, type UploadItem, type UploadItemStatus };
|
|
299
|
-
export { useAttachmentUrl, useUser, type UseUserState };
|
|
300
|
-
|
|
301
|
-
// Re-exported SDK types (so you don't have to add @poolse/sdk to your imports)
|
|
302
|
-
export type {
|
|
303
|
-
Attachment,
|
|
304
|
-
AttachmentStatus,
|
|
305
|
-
AttachmentUploadInput,
|
|
306
|
-
AttachmentUploadRequest,
|
|
307
|
-
AttachmentUploadResponse,
|
|
308
|
-
Conversation,
|
|
309
|
-
ConversationCreateRequest,
|
|
310
|
-
Me,
|
|
311
|
-
MemberRole,
|
|
312
|
-
Membership,
|
|
313
|
-
Message,
|
|
314
|
-
MessageCreateRequest,
|
|
315
|
-
MessageType,
|
|
316
|
-
PoolseConfig,
|
|
317
|
-
PoolseUserProfile,
|
|
318
|
-
RealtimeStatus,
|
|
319
|
-
Uuid,
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// Re-exported error classes
|
|
323
|
-
export { ApiError, AuthError, PoolseError, NetworkError, RateLimitedError };
|
|
324
|
-
```
|
|
65
|
+
| Hook | Returns |
|
|
66
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `usePoolse()` | The `Poolse` instance. |
|
|
68
|
+
| `useMe()` | `{ me, loading, error }` |
|
|
69
|
+
| `useConversation(id \| null)` | `{ conversation, loading, error, refetch }` |
|
|
70
|
+
| `useConversations()` | `{ conversations, loading, error, refetch, create, unreadCounts, markConversationRead }` |
|
|
71
|
+
| `useMessages(conversationId)` | `{ messages, loading, error, hasMore, loadMore, send, edit, delete, markReadUpTo, refetch }`, oldest first |
|
|
72
|
+
| `useThread(conversationId, rootId)` | `{ replies, loading, error, hasMore, loadMore, sendReply, edit, delete }` |
|
|
73
|
+
| `useReactions(messageId, { conversationId, initialReactions?, currentUserId? })` | `{ reactions, addReaction, removeReaction }` |
|
|
74
|
+
| `useTyping(conversationId)` | `{ typing: Set<externalId>, signalTyping }` |
|
|
75
|
+
| `usePresence(conversationId)` | `{ online: Set<externalId> }` |
|
|
76
|
+
| `useMembers(conversationId)` | `{ members, loading, error, refetch, addMembers(externalIds, { role? }), removeMember(externalId) }` |
|
|
77
|
+
| `useAttachmentUpload()` | `{ queue, attachment, uploading, error, upload(input), uploadAll(inputs), cancel(localId), remove(localId), reset }` |
|
|
78
|
+
| `useAttachmentUrl(id \| null)` | `{ url, loading, error }` (not renewed; URLs last one hour) |
|
|
79
|
+
| `useUser(externalId)` | `{ profile, loading }` |
|
|
80
|
+
| `useRealtimeStatus()` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'closed'` |
|
|
81
|
+
| `useReadReceipts()` | `{ visible, mode, shared, canChange, setShared }` |
|
|
82
|
+
| `useLiveCall(conversationId \| null)` | `{ call, participantIds, refresh }` |
|
|
83
|
+
| `useRoom(conversationId \| null, { livekit })` | `{ live, presentIds, joined, joining, join, leave, media, error }` |
|
|
84
|
+
| `useCallRoom(conversationId \| null, { livekit, … })` | Call media: participants, mic, camera, devices |
|
|
85
|
+
|
|
86
|
+
Helpers: `canEditOwnMessage`, `EDIT_WINDOW_MS`, `receiptsOf`,
|
|
87
|
+
`computeGroupPosition`, `sameGroup`, `sameDay`, `formatDayLabel`, plus
|
|
88
|
+
re-exports of the SDK's error classes, call-log helpers and wire types.
|
|
89
|
+
|
|
90
|
+
Notes that trip people up:
|
|
91
|
+
|
|
92
|
+
- `useTyping`, `usePresence`, `useUser` and `useMembers().removeMember` use
|
|
93
|
+
**your** user ids (`external_id`). `useReactions`' `currentUserId` and the
|
|
94
|
+
`reactions` map use **poolse** user ids (`me.id`).
|
|
95
|
+
- `useMessages().send` removes the optimistic row and rejects if the request
|
|
96
|
+
fails.
|
|
97
|
+
- `useAttachmentUpload().upload` takes `{ body, contentType, byteSize, filename? }`,
|
|
98
|
+
not a bare `File`. Send `item.attachment.id`, not `item.localId`.
|
|
99
|
+
- From 3.1, `useMessages` (the newest 50, merged in) and `useConversations`
|
|
100
|
+
reload from REST after the socket reconnects. The first connect doesn't
|
|
101
|
+
count as a reconnect.
|
|
102
|
+
- If the user is removed from a conversation, its mounted hooks stop
|
|
103
|
+
updating; hooks mounted later join a fresh channel.
|
|
325
104
|
|
|
326
105
|
## Links
|
|
327
106
|
|
|
328
|
-
-
|
|
329
|
-
-
|
|
330
|
-
- UI components — [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui)
|
|
331
|
-
- Source — <https://github.com/poolse-hq/js-sdk>
|
|
107
|
+
- Docs: <https://poolse.dev/docs/sdk/react>
|
|
108
|
+
- Source: <https://github.com/poolse-hq/js-sdk>
|
|
332
109
|
|
|
333
110
|
## License
|
|
334
111
|
|
|
335
|
-
|
|
112
|
+
Licensed under the [poolse SDK License](./LICENSE) from 3.0.0; versions
|
|
113
|
+
before 3.0.0 remain MIT.
|
package/dist/index.cjs
CHANGED
|
@@ -173,9 +173,21 @@ function useConversations() {
|
|
|
173
173
|
const fromMe = by_user_id === me.id;
|
|
174
174
|
setConversations((prev) => mergeUpdated(prev, conversation, fromMe));
|
|
175
175
|
});
|
|
176
|
+
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
177
|
+
let connectedBefore = wasConnected;
|
|
178
|
+
const offStatus = chat.realtime.onStatus((status) => {
|
|
179
|
+
const reconnected = status === "connected" && !wasConnected && connectedBefore;
|
|
180
|
+
connectedBefore ||= status === "connected";
|
|
181
|
+
if (reconnected) {
|
|
182
|
+
chat.conversations.list().then((list) => setConversations(list.data)).catch(() => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
wasConnected = status === "connected";
|
|
186
|
+
});
|
|
176
187
|
return () => {
|
|
177
188
|
offCreated();
|
|
178
189
|
offUpdated();
|
|
190
|
+
offStatus();
|
|
179
191
|
};
|
|
180
192
|
}, [chat, me]);
|
|
181
193
|
const markConversationRead = react.useCallback((conversationId) => {
|
|
@@ -382,11 +394,24 @@ function useMessages(conversationId) {
|
|
|
382
394
|
return updated;
|
|
383
395
|
});
|
|
384
396
|
});
|
|
397
|
+
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
398
|
+
let connectedBefore = wasConnected;
|
|
399
|
+
const offStatus = chat.realtime.onStatus((status) => {
|
|
400
|
+
const reconnected = status === "connected" && !wasConnected && connectedBefore;
|
|
401
|
+
wasConnected = status === "connected";
|
|
402
|
+
connectedBefore ||= wasConnected;
|
|
403
|
+
if (!reconnected || cancelled) return;
|
|
404
|
+
handle.list({ limit: PAGE_SIZE }).then((page) => {
|
|
405
|
+
if (!cancelled) setMessages((prev) => mergeLatest(prev, page.data));
|
|
406
|
+
}).catch(() => {
|
|
407
|
+
});
|
|
408
|
+
});
|
|
385
409
|
return () => {
|
|
386
410
|
cancelled = true;
|
|
387
411
|
offNew();
|
|
388
412
|
offUpdated();
|
|
389
413
|
offDeleted();
|
|
414
|
+
offStatus();
|
|
390
415
|
};
|
|
391
416
|
}, [chat, conversationId]);
|
|
392
417
|
const loadMore = react.useCallback(async () => {
|
|
@@ -527,6 +552,11 @@ function upsertById(prev, msg) {
|
|
|
527
552
|
next[idx] = msg;
|
|
528
553
|
return next;
|
|
529
554
|
}
|
|
555
|
+
function mergeLatest(prev, page) {
|
|
556
|
+
let next = prev;
|
|
557
|
+
for (const msg of page) next = upsertById(next, msg);
|
|
558
|
+
return next === prev ? prev : [...next].sort((a, b) => a.sequence - b.sequence);
|
|
559
|
+
}
|
|
530
560
|
function useReactions(messageId, opts) {
|
|
531
561
|
const chat = usePoolse();
|
|
532
562
|
const [reactions, setReactions] = react.useState(opts.initialReactions ?? {});
|
|
@@ -1276,9 +1306,11 @@ function useLiveCall(conversationId) {
|
|
|
1276
1306
|
apply(null);
|
|
1277
1307
|
});
|
|
1278
1308
|
let wasConnected = chat.realtime.getStatus() === "connected";
|
|
1309
|
+
let connectedBefore = wasConnected;
|
|
1279
1310
|
const offStatus = chat.realtime.onStatus((status) => {
|
|
1280
|
-
if (status === "connected" && !wasConnected) refresh();
|
|
1311
|
+
if (status === "connected" && !wasConnected && connectedBefore) refresh();
|
|
1281
1312
|
wasConnected = status === "connected";
|
|
1313
|
+
connectedBefore ||= wasConnected;
|
|
1282
1314
|
});
|
|
1283
1315
|
return () => {
|
|
1284
1316
|
offStarted();
|