@poolse/react 0.1.0 → 0.2.0-alpha.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/CHANGELOG.md +39 -0
- package/dist/index.cjs +169 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -5
- package/dist/index.d.ts +51 -5
- package/dist/index.js +169 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ 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
|
+
## [0.2.0] — 2026-06-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `useMessages.edit(messageId, body)` — optimistic edit with rollback
|
|
12
|
+
on server error. Server enforces sender-only authorization.
|
|
13
|
+
- `useMessages.delete(messageId)` — optimistic soft-delete (sets
|
|
14
|
+
`deleted_at` + null body locally, rolls back on error).
|
|
15
|
+
- `useMessages.markReadUpTo(messageId)` — wraps
|
|
16
|
+
`chat.conversations.one(id).messages.markRead`; advances the
|
|
17
|
+
caller's read cursor and triggers server-side read-receipt
|
|
18
|
+
broadcasts to other members.
|
|
19
|
+
- `useConversations` now exposes `unreadCounts: Record<convId, number>`
|
|
20
|
+
plus `markConversationRead(convId)` for sidebar-badge UIs.
|
|
21
|
+
- `useThread.edit(messageId, body)` / `useThread.delete(messageId)`
|
|
22
|
+
for inline reply editing inside `<ThreadView>`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `useMembers(conversationId)` now treats an empty string id as
|
|
27
|
+
"skip fetch" (returns `{ members: [], loading: false }`) so
|
|
28
|
+
callers can conditionally enable member loading without wrapping
|
|
29
|
+
in a `enabled ?` guard.
|
|
30
|
+
- `useMembers` subscribes to the new `member:read` realtime event
|
|
31
|
+
and advances the matching membership's `last_read_message_id` /
|
|
32
|
+
`last_read_at` in place. The sender's check-double glyph now
|
|
33
|
+
flips in real time without a refetch.
|
|
34
|
+
- Provider auto-resolves the SDK's new default `apiUrl` when none
|
|
35
|
+
is passed in config.
|
|
36
|
+
|
|
37
|
+
## [0.1.1] — 2026-06-01
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `useMessages.send()` now stamps the optimistic temp message's
|
|
42
|
+
`sender_id` with the current user's id (via `useMe` internally).
|
|
43
|
+
Without this, self-sent messages briefly rendered on the "other"
|
|
44
|
+
side of the chat until the realtime echo replaced them.
|
|
45
|
+
|
|
7
46
|
## [0.1.0] — 2026-06-01
|
|
8
47
|
|
|
9
48
|
First publishable release.
|
package/dist/index.cjs
CHANGED
|
@@ -19,7 +19,9 @@ function PoolseProvider({ config, children }) {
|
|
|
19
19
|
socketPath: initial.socketPath
|
|
20
20
|
};
|
|
21
21
|
return new sdk.Poolse({
|
|
22
|
-
apiUrl
|
|
22
|
+
// apiUrl is optional in PoolseConfig — only forward when set
|
|
23
|
+
// so the SDK can fall back to its hosted-endpoint default.
|
|
24
|
+
...initial.apiUrl !== void 0 ? { apiUrl: initial.apiUrl } : {},
|
|
23
25
|
// Always read the latest callbacks from the ref — they're free
|
|
24
26
|
// to change between renders.
|
|
25
27
|
getToken: () => configRef.current.getToken(),
|
|
@@ -162,7 +164,21 @@ function useConversations() {
|
|
|
162
164
|
});
|
|
163
165
|
return off;
|
|
164
166
|
}, [chat, me]);
|
|
165
|
-
|
|
167
|
+
const markConversationRead = react.useCallback((conversationId) => {
|
|
168
|
+
setConversations(
|
|
169
|
+
(prev) => prev.map((c) => c.id === conversationId ? { ...c, unread_count: 0 } : c)
|
|
170
|
+
);
|
|
171
|
+
}, []);
|
|
172
|
+
const unreadCounts = react.useMemo(() => {
|
|
173
|
+
const out = {};
|
|
174
|
+
for (const c of conversations) {
|
|
175
|
+
if (typeof c.unread_count === "number" && c.unread_count > 0) {
|
|
176
|
+
out[c.id] = c.unread_count;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}, [conversations]);
|
|
181
|
+
return { conversations, loading, error, refetch, create, unreadCounts, markConversationRead };
|
|
166
182
|
}
|
|
167
183
|
function prepend(prev, conv) {
|
|
168
184
|
const idx = prev.findIndex((c) => c.id === conv.id);
|
|
@@ -180,6 +196,12 @@ function useMembers(conversationId) {
|
|
|
180
196
|
membersRef.current = members;
|
|
181
197
|
const fetcher = react.useCallback(
|
|
182
198
|
async (signal) => {
|
|
199
|
+
if (!conversationId) {
|
|
200
|
+
setLoading(false);
|
|
201
|
+
setMembers([]);
|
|
202
|
+
setError(null);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
183
205
|
setLoading(true);
|
|
184
206
|
setError(null);
|
|
185
207
|
try {
|
|
@@ -199,6 +221,22 @@ function useMembers(conversationId) {
|
|
|
199
221
|
void fetcher(controller.signal);
|
|
200
222
|
return () => controller.abort();
|
|
201
223
|
}, [fetcher]);
|
|
224
|
+
react.useEffect(() => {
|
|
225
|
+
if (!conversationId) return;
|
|
226
|
+
const channel = chat.realtime.conversation(conversationId);
|
|
227
|
+
const off = channel.onMemberRead((evt) => {
|
|
228
|
+
setMembers(
|
|
229
|
+
(prev) => prev.map(
|
|
230
|
+
(m) => m.user_id === evt.user_id ? {
|
|
231
|
+
...m,
|
|
232
|
+
last_read_message_id: evt.last_read_message_id,
|
|
233
|
+
last_read_at: evt.last_read_at
|
|
234
|
+
} : m
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
return () => off();
|
|
239
|
+
}, [chat, conversationId]);
|
|
202
240
|
const refetch = react.useCallback(() => fetcher(), [fetcher]);
|
|
203
241
|
const addMembers = react.useCallback(
|
|
204
242
|
async (externalIds, opts = {}) => {
|
|
@@ -231,11 +269,14 @@ function useMembers(conversationId) {
|
|
|
231
269
|
var PAGE_SIZE = 50;
|
|
232
270
|
function useMessages(conversationId) {
|
|
233
271
|
const chat = usePoolse();
|
|
272
|
+
const { me } = useMe();
|
|
234
273
|
const [messages, setMessages] = react.useState([]);
|
|
235
274
|
const [loading, setLoading] = react.useState(true);
|
|
236
275
|
const [error, setError] = react.useState(null);
|
|
237
276
|
const [hasMore, setHasMore] = react.useState(true);
|
|
238
277
|
const oldestSequenceRef = react.useRef(null);
|
|
278
|
+
const meIdRef = react.useRef(me?.id ?? null);
|
|
279
|
+
meIdRef.current = me?.id ?? null;
|
|
239
280
|
react.useEffect(() => {
|
|
240
281
|
let cancelled = false;
|
|
241
282
|
setLoading(true);
|
|
@@ -298,9 +339,13 @@ function useMessages(conversationId) {
|
|
|
298
339
|
id,
|
|
299
340
|
tenant_id: "",
|
|
300
341
|
conversation_id: conversationId,
|
|
301
|
-
|
|
342
|
+
// Stamp the current user's id so the optimistic row renders
|
|
343
|
+
// on the right (self) side from the moment it appears. Null
|
|
344
|
+
// here would briefly flash the row on the "other" side until
|
|
345
|
+
// the realtime echo replaced it with the canonical row.
|
|
346
|
+
sender_id: meIdRef.current,
|
|
302
347
|
type: attrs.type ?? "text",
|
|
303
|
-
body: attrs.body,
|
|
348
|
+
body: attrs.body ?? null,
|
|
304
349
|
reply_to_id: attrs.reply_to_id ?? null,
|
|
305
350
|
thread_root_id: null,
|
|
306
351
|
mentions: attrs.mentions ?? [],
|
|
@@ -325,7 +370,69 @@ function useMessages(conversationId) {
|
|
|
325
370
|
},
|
|
326
371
|
[chat, conversationId]
|
|
327
372
|
);
|
|
328
|
-
|
|
373
|
+
const edit = react.useCallback(
|
|
374
|
+
async (messageId, body) => {
|
|
375
|
+
let previous;
|
|
376
|
+
setMessages(
|
|
377
|
+
(prev) => prev.map((m) => {
|
|
378
|
+
if (m.id !== messageId) return m;
|
|
379
|
+
previous = m;
|
|
380
|
+
return { ...m, body, edited_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
try {
|
|
384
|
+
const updated = await chat.messages.one(messageId).update({ body });
|
|
385
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? updated : m));
|
|
386
|
+
return updated;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (previous) {
|
|
389
|
+
const restore = previous;
|
|
390
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
[chat]
|
|
396
|
+
);
|
|
397
|
+
const deleteMessage = react.useCallback(
|
|
398
|
+
async (messageId) => {
|
|
399
|
+
let previous;
|
|
400
|
+
setMessages(
|
|
401
|
+
(prev) => prev.map((m) => {
|
|
402
|
+
if (m.id !== messageId) return m;
|
|
403
|
+
previous = m;
|
|
404
|
+
return { ...m, body: null, deleted_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
try {
|
|
408
|
+
await chat.messages.one(messageId).delete();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (previous) {
|
|
411
|
+
const restore = previous;
|
|
412
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
413
|
+
}
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
[chat]
|
|
418
|
+
);
|
|
419
|
+
const markReadUpTo = react.useCallback(
|
|
420
|
+
async (messageId) => {
|
|
421
|
+
await chat.conversations.one(conversationId).messages.markRead(messageId);
|
|
422
|
+
},
|
|
423
|
+
[chat, conversationId]
|
|
424
|
+
);
|
|
425
|
+
return {
|
|
426
|
+
messages,
|
|
427
|
+
loading,
|
|
428
|
+
error,
|
|
429
|
+
hasMore,
|
|
430
|
+
loadMore,
|
|
431
|
+
send,
|
|
432
|
+
edit,
|
|
433
|
+
delete: deleteMessage,
|
|
434
|
+
markReadUpTo
|
|
435
|
+
};
|
|
329
436
|
}
|
|
330
437
|
function upsertById(prev, msg) {
|
|
331
438
|
const idx = prev.findIndex((m) => m.id === msg.id);
|
|
@@ -469,7 +576,7 @@ function useThread(conversationId, rootMessageId) {
|
|
|
469
576
|
conversation_id: conversationId,
|
|
470
577
|
sender_id: null,
|
|
471
578
|
type: attrs.type ?? "text",
|
|
472
|
-
body: attrs.body,
|
|
579
|
+
body: attrs.body ?? null,
|
|
473
580
|
reply_to_id: rootMessageId,
|
|
474
581
|
thread_root_id: rootMessageId,
|
|
475
582
|
mentions: attrs.mentions ?? [],
|
|
@@ -496,7 +603,62 @@ function useThread(conversationId, rootMessageId) {
|
|
|
496
603
|
},
|
|
497
604
|
[chat, conversationId, rootMessageId]
|
|
498
605
|
);
|
|
499
|
-
|
|
606
|
+
const edit = react.useCallback(
|
|
607
|
+
async (messageId, body) => {
|
|
608
|
+
let previous;
|
|
609
|
+
setReplies(
|
|
610
|
+
(prev) => prev.map((m) => {
|
|
611
|
+
if (m.id !== messageId) return m;
|
|
612
|
+
previous = m;
|
|
613
|
+
return { ...m, body, edited_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
614
|
+
})
|
|
615
|
+
);
|
|
616
|
+
try {
|
|
617
|
+
const updated = await chat.messages.one(messageId).update({ body });
|
|
618
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? updated : m));
|
|
619
|
+
return updated;
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (previous) {
|
|
622
|
+
const restore = previous;
|
|
623
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
624
|
+
}
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
[chat]
|
|
629
|
+
);
|
|
630
|
+
const deleteReply = react.useCallback(
|
|
631
|
+
async (messageId) => {
|
|
632
|
+
let previous;
|
|
633
|
+
setReplies(
|
|
634
|
+
(prev) => prev.map((m) => {
|
|
635
|
+
if (m.id !== messageId) return m;
|
|
636
|
+
previous = m;
|
|
637
|
+
return { ...m, body: null, deleted_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
638
|
+
})
|
|
639
|
+
);
|
|
640
|
+
try {
|
|
641
|
+
await chat.messages.one(messageId).delete();
|
|
642
|
+
} catch (err) {
|
|
643
|
+
if (previous) {
|
|
644
|
+
const restore = previous;
|
|
645
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
646
|
+
}
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
[chat]
|
|
651
|
+
);
|
|
652
|
+
return {
|
|
653
|
+
replies,
|
|
654
|
+
loading,
|
|
655
|
+
error,
|
|
656
|
+
hasMore,
|
|
657
|
+
loadMore,
|
|
658
|
+
sendReply,
|
|
659
|
+
edit,
|
|
660
|
+
delete: deleteReply
|
|
661
|
+
};
|
|
500
662
|
}
|
|
501
663
|
function upsertById2(prev, msg) {
|
|
502
664
|
const idx = prev.findIndex((m) => m.id === msg.id);
|