@poolse/react 0.1.1 → 0.2.0-alpha.3
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 +30 -0
- package/dist/index.cjs +161 -6
- 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 +161 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -88,6 +88,20 @@ interface UseConversationsState {
|
|
|
88
88
|
error: Error | null;
|
|
89
89
|
refetch: () => Promise<void>;
|
|
90
90
|
create: (attrs: ConversationCreateRequest) => Promise<Conversation>;
|
|
91
|
+
/**
|
|
92
|
+
* Per-conversation unread count, keyed by conversation id. Sourced
|
|
93
|
+
* from `Conversation.unread_count` on the initial fetch and kept
|
|
94
|
+
* fresh as the caller marks conversations read (via `markRead`).
|
|
95
|
+
*
|
|
96
|
+
* Note: cross-conversation incrementing on a new inbound message
|
|
97
|
+
* requires subscribing to every conversation channel, which the SDK
|
|
98
|
+
* doesn't do by default. Counts in this map are accurate for the
|
|
99
|
+
* conversation the caller has open + correct at fetch time for the
|
|
100
|
+
* rest; refetch on a fresh tab focus for true reconciliation.
|
|
101
|
+
*/
|
|
102
|
+
unreadCounts: Record<Uuid, number>;
|
|
103
|
+
/** Force a conversation's unread to 0 — invoked by `markReadUpTo`. */
|
|
104
|
+
markConversationRead: (conversationId: Uuid) => void;
|
|
91
105
|
}
|
|
92
106
|
/**
|
|
93
107
|
* List of conversations the current End User can see.
|
|
@@ -125,11 +139,13 @@ interface UseMembersState {
|
|
|
125
139
|
}
|
|
126
140
|
/**
|
|
127
141
|
* Live-ish membership list for a conversation. Initial REST fetch on
|
|
128
|
-
* mount; mutation helpers update local state in place.
|
|
129
|
-
*
|
|
130
|
-
* `member:
|
|
131
|
-
*
|
|
132
|
-
*
|
|
142
|
+
* mount; mutation helpers update local state in place.
|
|
143
|
+
*
|
|
144
|
+
* Realtime: subscribes to `member:read` on the conversation channel
|
|
145
|
+
* and advances the matching membership's `last_read_message_id` /
|
|
146
|
+
* `last_read_at` so the sender's read-receipt glyph flips from "sent"
|
|
147
|
+
* to "read" without a refetch. `member:joined` / `member:left` aren't
|
|
148
|
+
* broadcast yet — for cross-client adds/removes, call `refetch()`.
|
|
133
149
|
*/
|
|
134
150
|
declare function useMembers(conversationId: Uuid): UseMembersState;
|
|
135
151
|
|
|
@@ -143,6 +159,26 @@ interface UseMessagesState {
|
|
|
143
159
|
loadMore: () => Promise<void>;
|
|
144
160
|
/** Optimistically append + POST. Replaces the optimistic row with the server response on success. */
|
|
145
161
|
send: (attrs: MessageCreateRequest) => Promise<Message>;
|
|
162
|
+
/**
|
|
163
|
+
* Edit a message you previously sent. Optimistically updates the
|
|
164
|
+
* local row, rolls back on server error. Server enforces
|
|
165
|
+
* sender-only authorization — calling this for a message you didn't
|
|
166
|
+
* send will throw an ApiError.
|
|
167
|
+
*/
|
|
168
|
+
edit: (messageId: Uuid, body: string) => Promise<Message>;
|
|
169
|
+
/**
|
|
170
|
+
* Soft-delete a message you sent (or that you have admin authority
|
|
171
|
+
* over). Optimistically applies the tombstone (`deleted_at` + null
|
|
172
|
+
* body) locally, rolls back on error.
|
|
173
|
+
*/
|
|
174
|
+
delete: (messageId: Uuid) => Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Advance the caller's read cursor to `messageId`. Server emits
|
|
177
|
+
* read-receipt updates to other members; the conversation's
|
|
178
|
+
* unread count drops to zero. No local state mutation — this is
|
|
179
|
+
* "tell the server we read up to here".
|
|
180
|
+
*/
|
|
181
|
+
markReadUpTo: (messageId: Uuid) => Promise<void>;
|
|
146
182
|
}
|
|
147
183
|
/**
|
|
148
184
|
* Live message list for `conversationId`. Combines:
|
|
@@ -225,6 +261,16 @@ interface UseThreadState {
|
|
|
225
261
|
* `MessageCreateRequest` fields) as you would to `useMessages.send`.
|
|
226
262
|
*/
|
|
227
263
|
sendReply: (attrs: Omit<MessageCreateRequest, 'reply_to_id'>) => Promise<Message>;
|
|
264
|
+
/**
|
|
265
|
+
* Edit a reply (or the root message). Optimistic with rollback —
|
|
266
|
+
* mirrors `useMessages.edit`. Server enforces sender-only authz.
|
|
267
|
+
*/
|
|
268
|
+
edit: (messageId: Uuid, body: string) => Promise<Message>;
|
|
269
|
+
/**
|
|
270
|
+
* Soft-delete a reply (or the root message). Optimistic tombstone
|
|
271
|
+
* (`deleted_at` + null body) — mirrors `useMessages.delete`.
|
|
272
|
+
*/
|
|
273
|
+
delete: (messageId: Uuid) => Promise<void>;
|
|
228
274
|
}
|
|
229
275
|
/**
|
|
230
276
|
* Thread view rooted at `rootMessageId`. Loads replies oldest-first
|
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,20 @@ interface UseConversationsState {
|
|
|
88
88
|
error: Error | null;
|
|
89
89
|
refetch: () => Promise<void>;
|
|
90
90
|
create: (attrs: ConversationCreateRequest) => Promise<Conversation>;
|
|
91
|
+
/**
|
|
92
|
+
* Per-conversation unread count, keyed by conversation id. Sourced
|
|
93
|
+
* from `Conversation.unread_count` on the initial fetch and kept
|
|
94
|
+
* fresh as the caller marks conversations read (via `markRead`).
|
|
95
|
+
*
|
|
96
|
+
* Note: cross-conversation incrementing on a new inbound message
|
|
97
|
+
* requires subscribing to every conversation channel, which the SDK
|
|
98
|
+
* doesn't do by default. Counts in this map are accurate for the
|
|
99
|
+
* conversation the caller has open + correct at fetch time for the
|
|
100
|
+
* rest; refetch on a fresh tab focus for true reconciliation.
|
|
101
|
+
*/
|
|
102
|
+
unreadCounts: Record<Uuid, number>;
|
|
103
|
+
/** Force a conversation's unread to 0 — invoked by `markReadUpTo`. */
|
|
104
|
+
markConversationRead: (conversationId: Uuid) => void;
|
|
91
105
|
}
|
|
92
106
|
/**
|
|
93
107
|
* List of conversations the current End User can see.
|
|
@@ -125,11 +139,13 @@ interface UseMembersState {
|
|
|
125
139
|
}
|
|
126
140
|
/**
|
|
127
141
|
* Live-ish membership list for a conversation. Initial REST fetch on
|
|
128
|
-
* mount; mutation helpers update local state in place.
|
|
129
|
-
*
|
|
130
|
-
* `member:
|
|
131
|
-
*
|
|
132
|
-
*
|
|
142
|
+
* mount; mutation helpers update local state in place.
|
|
143
|
+
*
|
|
144
|
+
* Realtime: subscribes to `member:read` on the conversation channel
|
|
145
|
+
* and advances the matching membership's `last_read_message_id` /
|
|
146
|
+
* `last_read_at` so the sender's read-receipt glyph flips from "sent"
|
|
147
|
+
* to "read" without a refetch. `member:joined` / `member:left` aren't
|
|
148
|
+
* broadcast yet — for cross-client adds/removes, call `refetch()`.
|
|
133
149
|
*/
|
|
134
150
|
declare function useMembers(conversationId: Uuid): UseMembersState;
|
|
135
151
|
|
|
@@ -143,6 +159,26 @@ interface UseMessagesState {
|
|
|
143
159
|
loadMore: () => Promise<void>;
|
|
144
160
|
/** Optimistically append + POST. Replaces the optimistic row with the server response on success. */
|
|
145
161
|
send: (attrs: MessageCreateRequest) => Promise<Message>;
|
|
162
|
+
/**
|
|
163
|
+
* Edit a message you previously sent. Optimistically updates the
|
|
164
|
+
* local row, rolls back on server error. Server enforces
|
|
165
|
+
* sender-only authorization — calling this for a message you didn't
|
|
166
|
+
* send will throw an ApiError.
|
|
167
|
+
*/
|
|
168
|
+
edit: (messageId: Uuid, body: string) => Promise<Message>;
|
|
169
|
+
/**
|
|
170
|
+
* Soft-delete a message you sent (or that you have admin authority
|
|
171
|
+
* over). Optimistically applies the tombstone (`deleted_at` + null
|
|
172
|
+
* body) locally, rolls back on error.
|
|
173
|
+
*/
|
|
174
|
+
delete: (messageId: Uuid) => Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Advance the caller's read cursor to `messageId`. Server emits
|
|
177
|
+
* read-receipt updates to other members; the conversation's
|
|
178
|
+
* unread count drops to zero. No local state mutation — this is
|
|
179
|
+
* "tell the server we read up to here".
|
|
180
|
+
*/
|
|
181
|
+
markReadUpTo: (messageId: Uuid) => Promise<void>;
|
|
146
182
|
}
|
|
147
183
|
/**
|
|
148
184
|
* Live message list for `conversationId`. Combines:
|
|
@@ -225,6 +261,16 @@ interface UseThreadState {
|
|
|
225
261
|
* `MessageCreateRequest` fields) as you would to `useMessages.send`.
|
|
226
262
|
*/
|
|
227
263
|
sendReply: (attrs: Omit<MessageCreateRequest, 'reply_to_id'>) => Promise<Message>;
|
|
264
|
+
/**
|
|
265
|
+
* Edit a reply (or the root message). Optimistic with rollback —
|
|
266
|
+
* mirrors `useMessages.edit`. Server enforces sender-only authz.
|
|
267
|
+
*/
|
|
268
|
+
edit: (messageId: Uuid, body: string) => Promise<Message>;
|
|
269
|
+
/**
|
|
270
|
+
* Soft-delete a reply (or the root message). Optimistic tombstone
|
|
271
|
+
* (`deleted_at` + null body) — mirrors `useMessages.delete`.
|
|
272
|
+
*/
|
|
273
|
+
delete: (messageId: Uuid) => Promise<void>;
|
|
228
274
|
}
|
|
229
275
|
/**
|
|
230
276
|
* Thread view rooted at `rootMessageId`. Loads replies oldest-first
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ function PoolseProvider({ config, children }) {
|
|
|
18
18
|
socketPath: initial.socketPath
|
|
19
19
|
};
|
|
20
20
|
return new Poolse({
|
|
21
|
-
apiUrl
|
|
21
|
+
// apiUrl is optional in PoolseConfig — only forward when set
|
|
22
|
+
// so the SDK can fall back to its hosted-endpoint default.
|
|
23
|
+
...initial.apiUrl !== void 0 ? { apiUrl: initial.apiUrl } : {},
|
|
22
24
|
// Always read the latest callbacks from the ref — they're free
|
|
23
25
|
// to change between renders.
|
|
24
26
|
getToken: () => configRef.current.getToken(),
|
|
@@ -161,7 +163,21 @@ function useConversations() {
|
|
|
161
163
|
});
|
|
162
164
|
return off;
|
|
163
165
|
}, [chat, me]);
|
|
164
|
-
|
|
166
|
+
const markConversationRead = useCallback((conversationId) => {
|
|
167
|
+
setConversations(
|
|
168
|
+
(prev) => prev.map((c) => c.id === conversationId ? { ...c, unread_count: 0 } : c)
|
|
169
|
+
);
|
|
170
|
+
}, []);
|
|
171
|
+
const unreadCounts = useMemo(() => {
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const c of conversations) {
|
|
174
|
+
if (typeof c.unread_count === "number" && c.unread_count > 0) {
|
|
175
|
+
out[c.id] = c.unread_count;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}, [conversations]);
|
|
180
|
+
return { conversations, loading, error, refetch, create, unreadCounts, markConversationRead };
|
|
165
181
|
}
|
|
166
182
|
function prepend(prev, conv) {
|
|
167
183
|
const idx = prev.findIndex((c) => c.id === conv.id);
|
|
@@ -179,6 +195,12 @@ function useMembers(conversationId) {
|
|
|
179
195
|
membersRef.current = members;
|
|
180
196
|
const fetcher = useCallback(
|
|
181
197
|
async (signal) => {
|
|
198
|
+
if (!conversationId) {
|
|
199
|
+
setLoading(false);
|
|
200
|
+
setMembers([]);
|
|
201
|
+
setError(null);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
182
204
|
setLoading(true);
|
|
183
205
|
setError(null);
|
|
184
206
|
try {
|
|
@@ -198,6 +220,22 @@ function useMembers(conversationId) {
|
|
|
198
220
|
void fetcher(controller.signal);
|
|
199
221
|
return () => controller.abort();
|
|
200
222
|
}, [fetcher]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!conversationId) return;
|
|
225
|
+
const channel = chat.realtime.conversation(conversationId);
|
|
226
|
+
const off = channel.onMemberRead((evt) => {
|
|
227
|
+
setMembers(
|
|
228
|
+
(prev) => prev.map(
|
|
229
|
+
(m) => m.user_id === evt.user_id ? {
|
|
230
|
+
...m,
|
|
231
|
+
last_read_message_id: evt.last_read_message_id,
|
|
232
|
+
last_read_at: evt.last_read_at
|
|
233
|
+
} : m
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
return () => off();
|
|
238
|
+
}, [chat, conversationId]);
|
|
201
239
|
const refetch = useCallback(() => fetcher(), [fetcher]);
|
|
202
240
|
const addMembers = useCallback(
|
|
203
241
|
async (externalIds, opts = {}) => {
|
|
@@ -306,7 +344,7 @@ function useMessages(conversationId) {
|
|
|
306
344
|
// the realtime echo replaced it with the canonical row.
|
|
307
345
|
sender_id: meIdRef.current,
|
|
308
346
|
type: attrs.type ?? "text",
|
|
309
|
-
body: attrs.body,
|
|
347
|
+
body: attrs.body ?? null,
|
|
310
348
|
reply_to_id: attrs.reply_to_id ?? null,
|
|
311
349
|
thread_root_id: null,
|
|
312
350
|
mentions: attrs.mentions ?? [],
|
|
@@ -331,7 +369,69 @@ function useMessages(conversationId) {
|
|
|
331
369
|
},
|
|
332
370
|
[chat, conversationId]
|
|
333
371
|
);
|
|
334
|
-
|
|
372
|
+
const edit = useCallback(
|
|
373
|
+
async (messageId, body) => {
|
|
374
|
+
let previous;
|
|
375
|
+
setMessages(
|
|
376
|
+
(prev) => prev.map((m) => {
|
|
377
|
+
if (m.id !== messageId) return m;
|
|
378
|
+
previous = m;
|
|
379
|
+
return { ...m, body, edited_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
try {
|
|
383
|
+
const updated = await chat.messages.one(messageId).update({ body });
|
|
384
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? updated : m));
|
|
385
|
+
return updated;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (previous) {
|
|
388
|
+
const restore = previous;
|
|
389
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
390
|
+
}
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
[chat]
|
|
395
|
+
);
|
|
396
|
+
const deleteMessage = useCallback(
|
|
397
|
+
async (messageId) => {
|
|
398
|
+
let previous;
|
|
399
|
+
setMessages(
|
|
400
|
+
(prev) => prev.map((m) => {
|
|
401
|
+
if (m.id !== messageId) return m;
|
|
402
|
+
previous = m;
|
|
403
|
+
return { ...m, body: null, deleted_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
try {
|
|
407
|
+
await chat.messages.one(messageId).delete();
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (previous) {
|
|
410
|
+
const restore = previous;
|
|
411
|
+
setMessages((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
412
|
+
}
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
[chat]
|
|
417
|
+
);
|
|
418
|
+
const markReadUpTo = useCallback(
|
|
419
|
+
async (messageId) => {
|
|
420
|
+
await chat.conversations.one(conversationId).messages.markRead(messageId);
|
|
421
|
+
},
|
|
422
|
+
[chat, conversationId]
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
messages,
|
|
426
|
+
loading,
|
|
427
|
+
error,
|
|
428
|
+
hasMore,
|
|
429
|
+
loadMore,
|
|
430
|
+
send,
|
|
431
|
+
edit,
|
|
432
|
+
delete: deleteMessage,
|
|
433
|
+
markReadUpTo
|
|
434
|
+
};
|
|
335
435
|
}
|
|
336
436
|
function upsertById(prev, msg) {
|
|
337
437
|
const idx = prev.findIndex((m) => m.id === msg.id);
|
|
@@ -475,7 +575,7 @@ function useThread(conversationId, rootMessageId) {
|
|
|
475
575
|
conversation_id: conversationId,
|
|
476
576
|
sender_id: null,
|
|
477
577
|
type: attrs.type ?? "text",
|
|
478
|
-
body: attrs.body,
|
|
578
|
+
body: attrs.body ?? null,
|
|
479
579
|
reply_to_id: rootMessageId,
|
|
480
580
|
thread_root_id: rootMessageId,
|
|
481
581
|
mentions: attrs.mentions ?? [],
|
|
@@ -502,7 +602,62 @@ function useThread(conversationId, rootMessageId) {
|
|
|
502
602
|
},
|
|
503
603
|
[chat, conversationId, rootMessageId]
|
|
504
604
|
);
|
|
505
|
-
|
|
605
|
+
const edit = useCallback(
|
|
606
|
+
async (messageId, body) => {
|
|
607
|
+
let previous;
|
|
608
|
+
setReplies(
|
|
609
|
+
(prev) => prev.map((m) => {
|
|
610
|
+
if (m.id !== messageId) return m;
|
|
611
|
+
previous = m;
|
|
612
|
+
return { ...m, body, edited_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
613
|
+
})
|
|
614
|
+
);
|
|
615
|
+
try {
|
|
616
|
+
const updated = await chat.messages.one(messageId).update({ body });
|
|
617
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? updated : m));
|
|
618
|
+
return updated;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (previous) {
|
|
621
|
+
const restore = previous;
|
|
622
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
623
|
+
}
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
[chat]
|
|
628
|
+
);
|
|
629
|
+
const deleteReply = useCallback(
|
|
630
|
+
async (messageId) => {
|
|
631
|
+
let previous;
|
|
632
|
+
setReplies(
|
|
633
|
+
(prev) => prev.map((m) => {
|
|
634
|
+
if (m.id !== messageId) return m;
|
|
635
|
+
previous = m;
|
|
636
|
+
return { ...m, body: null, deleted_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
try {
|
|
640
|
+
await chat.messages.one(messageId).delete();
|
|
641
|
+
} catch (err) {
|
|
642
|
+
if (previous) {
|
|
643
|
+
const restore = previous;
|
|
644
|
+
setReplies((prev) => prev.map((m) => m.id === messageId ? restore : m));
|
|
645
|
+
}
|
|
646
|
+
throw err;
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
[chat]
|
|
650
|
+
);
|
|
651
|
+
return {
|
|
652
|
+
replies,
|
|
653
|
+
loading,
|
|
654
|
+
error,
|
|
655
|
+
hasMore,
|
|
656
|
+
loadMore,
|
|
657
|
+
sendReply,
|
|
658
|
+
edit,
|
|
659
|
+
delete: deleteReply
|
|
660
|
+
};
|
|
506
661
|
}
|
|
507
662
|
function upsertById2(prev, msg) {
|
|
508
663
|
const idx = prev.findIndex((m) => m.id === msg.id);
|