@poolse/react 0.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 +25 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.cjs +695 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +340 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +661 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput } from '@poolse/sdk';
|
|
3
|
+
export { ApiError, Attachment, AttachmentStatus, AttachmentUploadInput, AttachmentUploadRequest, AttachmentUploadResponse, AuthError, Conversation, ConversationCreateRequest, Me, MemberRole, Membership, Message, MessageCreateRequest, MessageType, NetworkError, PoolseConfig, PoolseError, RateLimitedError, RealtimeStatus, Uuid } from '@poolse/sdk';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
interface PoolseProviderProps {
|
|
7
|
+
/**
|
|
8
|
+
* SDK configuration. The provider builds the `Poolse` instance ONCE
|
|
9
|
+
* per mount and reads `getToken` (and the other function-typed
|
|
10
|
+
* options) from a ref on each call, so it's safe to pass a fresh
|
|
11
|
+
* callback or a fresh object literal on every render.
|
|
12
|
+
*
|
|
13
|
+
* Re-mount the provider (give it a new `key`) when you genuinely
|
|
14
|
+
* want a fresh client — typically after a sign-out, a tenant swap,
|
|
15
|
+
* or an `apiUrl` change. Mutating `apiUrl` / `wsUrl` / `socketPath`
|
|
16
|
+
* after mount is a no-op (the values are captured at construction
|
|
17
|
+
* time); dev mode logs a warning when this happens.
|
|
18
|
+
*/
|
|
19
|
+
config: PoolseConfig;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Top-level provider. Wrap your tree once with this; every hook in
|
|
24
|
+
* `@poolse/react` reads from the context.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <PoolseProvider config={{apiUrl, getToken}}>
|
|
28
|
+
* <App />
|
|
29
|
+
* </PoolseProvider>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The `Poolse` instance is destroyed on unmount — closes the WebSocket
|
|
33
|
+
* and drops every joined channel.
|
|
34
|
+
*/
|
|
35
|
+
declare function PoolseProvider({ config, children }: PoolseProviderProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
/**
|
|
37
|
+
* Access the underlying `Poolse` instance. Most app code should prefer
|
|
38
|
+
* higher-level hooks (`useMessages`, `useTyping`, etc.) but the raw
|
|
39
|
+
* client is here for escape-hatch usage (custom REST calls, direct
|
|
40
|
+
* realtime control).
|
|
41
|
+
*/
|
|
42
|
+
declare function usePoolse(): Poolse;
|
|
43
|
+
|
|
44
|
+
interface UseMeState {
|
|
45
|
+
me: Me | null;
|
|
46
|
+
loading: boolean;
|
|
47
|
+
error: Error | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The currently-signed-in End User (`/v1/me`). Fetches once on mount
|
|
51
|
+
* and never re-fetches automatically — user identity is stable for
|
|
52
|
+
* the JWT's lifetime.
|
|
53
|
+
*
|
|
54
|
+
* Most apps won't need this; the JWT subject is enough. Useful when
|
|
55
|
+
* the UI needs the display name / external_id without re-asking the
|
|
56
|
+
* Customer's backend.
|
|
57
|
+
*/
|
|
58
|
+
declare function useMe(): UseMeState;
|
|
59
|
+
|
|
60
|
+
interface UseConversationState {
|
|
61
|
+
/** The fetched conversation, or `null` while loading or on error. */
|
|
62
|
+
conversation: Conversation | null;
|
|
63
|
+
loading: boolean;
|
|
64
|
+
error: Error | null;
|
|
65
|
+
/** Re-run the GET. Use after a `chat.conversations.one(id).update(...)` to refresh state. */
|
|
66
|
+
refetch: () => Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fetch a single conversation by id. Re-fetches when `id` changes
|
|
70
|
+
* (e.g. routing to a different conversation in the same component).
|
|
71
|
+
*
|
|
72
|
+
* Doesn't subscribe to realtime updates yet — the backend doesn't
|
|
73
|
+
* broadcast `conversation:updated` to user channels. Use `refetch()`
|
|
74
|
+
* after a mutation if you need fresh state in the same render cycle.
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* const { conversation, loading } = useConversation(convId);
|
|
78
|
+
* if (loading) return <Spinner />;
|
|
79
|
+
* if (!conversation) return <NotFound />;
|
|
80
|
+
* return <h1>{conversation.name ?? 'Untitled'}</h1>;
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
declare function useConversation(id: Uuid | null): UseConversationState;
|
|
84
|
+
|
|
85
|
+
interface UseConversationsState {
|
|
86
|
+
conversations: Conversation[];
|
|
87
|
+
loading: boolean;
|
|
88
|
+
error: Error | null;
|
|
89
|
+
refetch: () => Promise<void>;
|
|
90
|
+
create: (attrs: ConversationCreateRequest) => Promise<Conversation>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* List of conversations the current End User can see.
|
|
94
|
+
*
|
|
95
|
+
* On mount: REST-fetches the list and subscribes to the user's
|
|
96
|
+
* `user:<id>` channel for `conversation:created` events — fired by
|
|
97
|
+
* the server both when this user creates a conversation AND when
|
|
98
|
+
* someone else adds them to one. New rows are prepended to local
|
|
99
|
+
* state with id-based dedup so an optimistic `create()` followed by
|
|
100
|
+
* the realtime echo doesn't insert twice.
|
|
101
|
+
*
|
|
102
|
+
* `refetch()` is still here for explicit re-syncs (e.g. after a
|
|
103
|
+
* mutation that the SDK doesn't yet observe — name changes,
|
|
104
|
+
* settings updates). `create()` returns the canonical server row.
|
|
105
|
+
*/
|
|
106
|
+
declare function useConversations(): UseConversationsState;
|
|
107
|
+
|
|
108
|
+
interface UseMembersState {
|
|
109
|
+
members: Membership[];
|
|
110
|
+
loading: boolean;
|
|
111
|
+
error: Error | null;
|
|
112
|
+
refetch: () => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Add one or more users by external_id. The returned memberships
|
|
115
|
+
* are appended to local state on success.
|
|
116
|
+
*/
|
|
117
|
+
addMembers: (externalIds: string[], opts?: {
|
|
118
|
+
role?: MemberRole;
|
|
119
|
+
}) => Promise<Membership[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Remove a member by user_id. Optimistically removes from state;
|
|
122
|
+
* rolls back on server error.
|
|
123
|
+
*/
|
|
124
|
+
removeMember: (userId: Uuid) => Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Live-ish membership list for a conversation. Initial REST fetch on
|
|
128
|
+
* mount; mutation helpers update local state in place. No realtime
|
|
129
|
+
* sync yet — the backend doesn't broadcast `member:joined` /
|
|
130
|
+
* `member:left` events. For now, callers who depend on
|
|
131
|
+
* cross-client member updates should poll with `refetch()` or
|
|
132
|
+
* remount the hook.
|
|
133
|
+
*/
|
|
134
|
+
declare function useMembers(conversationId: Uuid): UseMembersState;
|
|
135
|
+
|
|
136
|
+
interface UseMessagesState {
|
|
137
|
+
/** Newest-last array. The wire returns newest-first; we reverse so list rendering matches reading order. */
|
|
138
|
+
messages: Message[];
|
|
139
|
+
loading: boolean;
|
|
140
|
+
error: Error | null;
|
|
141
|
+
hasMore: boolean;
|
|
142
|
+
/** Load an older page (lower sequence numbers). Idempotent if no older messages exist. */
|
|
143
|
+
loadMore: () => Promise<void>;
|
|
144
|
+
/** Optimistically append + POST. Replaces the optimistic row with the server response on success. */
|
|
145
|
+
send: (attrs: MessageCreateRequest) => Promise<Message>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Live message list for `conversationId`. Combines:
|
|
149
|
+
*
|
|
150
|
+
* * Initial REST fetch (newest 50) on mount.
|
|
151
|
+
* * WebSocket subscription for `message:new`, `message:updated`,
|
|
152
|
+
* `message:deleted` — applied in-place to the cached array.
|
|
153
|
+
* * Optimistic-send via `send()` — the message appears immediately
|
|
154
|
+
* under the SAME id the server will assign (we pre-generate it
|
|
155
|
+
* client-side and pass it through), so the realtime echo and the
|
|
156
|
+
* REST response both arrive carrying that id and get deduped by
|
|
157
|
+
* `findIndex(m => m.id === msg.id)`. No heuristic match, no race.
|
|
158
|
+
*
|
|
159
|
+
* Re-mounting with the same `conversationId` reuses the underlying
|
|
160
|
+
* channel — no extra socket traffic.
|
|
161
|
+
*/
|
|
162
|
+
declare function useMessages(conversationId: string): UseMessagesState;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Map of emoji → list of user_ids who reacted with it. Matches the
|
|
166
|
+
* shape of `Message.reactions` so initial seeding from a message you
|
|
167
|
+
* already have in state is a direct assignment.
|
|
168
|
+
*/
|
|
169
|
+
type ReactionMap = Record<string, Uuid[]>;
|
|
170
|
+
interface UseReactionsState {
|
|
171
|
+
reactions: ReactionMap;
|
|
172
|
+
/** Add the caller's reaction (idempotent server-side). */
|
|
173
|
+
addReaction: (emoji: string) => Promise<void>;
|
|
174
|
+
/** Remove the caller's reaction. No-op if not present. */
|
|
175
|
+
removeReaction: (emoji: string) => Promise<void>;
|
|
176
|
+
}
|
|
177
|
+
interface UseReactionsOptions {
|
|
178
|
+
/**
|
|
179
|
+
* Required for the in-channel subscription — the realtime layer
|
|
180
|
+
* routes reaction events through the conversation channel, not a
|
|
181
|
+
* per-message channel. Pass the conversation id you already know.
|
|
182
|
+
*/
|
|
183
|
+
conversationId: Uuid;
|
|
184
|
+
/**
|
|
185
|
+
* Seed the local reaction map from a message you already have
|
|
186
|
+
* (e.g. from `useMessages`). Without this the hook starts empty
|
|
187
|
+
* and only sees deltas from the realtime stream.
|
|
188
|
+
*/
|
|
189
|
+
initialReactions?: ReactionMap;
|
|
190
|
+
/**
|
|
191
|
+
* Current user's id. When provided, optimistic mutate updates the
|
|
192
|
+
* local map immediately for that user; when omitted, we wait for
|
|
193
|
+
* the server's `reaction:added` echo to update state.
|
|
194
|
+
*/
|
|
195
|
+
currentUserId?: Uuid | null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Live reaction map for a single message. Subscribes to
|
|
199
|
+
* `reaction:added` / `reaction:removed` realtime events filtered to
|
|
200
|
+
* `messageId`, and provides mutation helpers.
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* const { reactions, addReaction } = useReactions(msg.id, {
|
|
204
|
+
* conversationId: msg.conversation_id,
|
|
205
|
+
* initialReactions: msg.reactions,
|
|
206
|
+
* currentUserId: me?.id,
|
|
207
|
+
* });
|
|
208
|
+
* <button onClick={() => addReaction('🎉')}>🎉 {reactions['🎉']?.length ?? 0}</button>
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
declare function useReactions(messageId: Uuid, opts: UseReactionsOptions): UseReactionsState;
|
|
212
|
+
|
|
213
|
+
interface UseThreadState {
|
|
214
|
+
/** Replies, oldest-first (the wire returns oldest-first; we don't flip). */
|
|
215
|
+
replies: Message[];
|
|
216
|
+
loading: boolean;
|
|
217
|
+
error: Error | null;
|
|
218
|
+
/** More older-than-current-page replies available on the server. */
|
|
219
|
+
hasMore: boolean;
|
|
220
|
+
/** Load the next page after the current newest reply. Idempotent if no more. */
|
|
221
|
+
loadMore: () => Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Send a reply into this thread. Fills `reply_to_id` with the root
|
|
224
|
+
* message id automatically; pass `body` (and any other
|
|
225
|
+
* `MessageCreateRequest` fields) as you would to `useMessages.send`.
|
|
226
|
+
*/
|
|
227
|
+
sendReply: (attrs: Omit<MessageCreateRequest, 'reply_to_id'>) => Promise<Message>;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Thread view rooted at `rootMessageId`. Loads replies oldest-first
|
|
231
|
+
* with cursor pagination (`after: sequence`), subscribes to the
|
|
232
|
+
* conversation channel's `message:new` event filtered by
|
|
233
|
+
* `thread_root_id === rootMessageId`, and provides a `sendReply`
|
|
234
|
+
* that wires `reply_to_id` for you.
|
|
235
|
+
*
|
|
236
|
+
* Like `useMessages`, the optimistic send uses the same id the
|
|
237
|
+
* server will assign (auto-generated by the SDK), so the realtime
|
|
238
|
+
* echo just upserts in place — no double-render.
|
|
239
|
+
*/
|
|
240
|
+
declare function useThread(conversationId: Uuid, rootMessageId: Uuid): UseThreadState;
|
|
241
|
+
|
|
242
|
+
interface UseTypingState {
|
|
243
|
+
/** Set of user_ids currently typing. */
|
|
244
|
+
typing: Set<Uuid>;
|
|
245
|
+
/**
|
|
246
|
+
* Send a typing ping to the server. Debounced inside the SDK — the
|
|
247
|
+
* server squashes `typing:start` to one push per (user, conversation,
|
|
248
|
+
* window). Call as often as you want from input handlers.
|
|
249
|
+
*/
|
|
250
|
+
signalTyping: () => void;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Tracks who's typing in `conversationId`. Subscribes to the
|
|
254
|
+
* `typing:start` / `typing:stop` broadcasts; auto-cleans up entries
|
|
255
|
+
* when stop fires.
|
|
256
|
+
*
|
|
257
|
+
* `signalTyping` debounces on the client side to once per 500 ms so a
|
|
258
|
+
* fast-typing user doesn't flood the channel — poolse does its own
|
|
259
|
+
* server-side debounce too, so both layers are safe.
|
|
260
|
+
*/
|
|
261
|
+
declare function useTyping(conversationId: string): UseTypingState;
|
|
262
|
+
|
|
263
|
+
interface UsePresenceState {
|
|
264
|
+
/** Set of user_ids currently online in this conversation. */
|
|
265
|
+
online: Set<Uuid>;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Who's currently joined to `conversationId`'s channel. Backed by
|
|
269
|
+
* Phoenix.Presence broadcasts (`presence_state` snapshot on join,
|
|
270
|
+
* `presence_diff` on changes).
|
|
271
|
+
*/
|
|
272
|
+
declare function usePresence(conversationId: string): UsePresenceState;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Connection status of the underlying WebSocket. Useful for showing a
|
|
276
|
+
* "reconnecting…" banner without coupling to socket internals.
|
|
277
|
+
*
|
|
278
|
+
* Returns one of: `'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closed'`.
|
|
279
|
+
*/
|
|
280
|
+
declare function useRealtimeStatus(): RealtimeStatus;
|
|
281
|
+
|
|
282
|
+
interface UseAttachmentUploadState {
|
|
283
|
+
/** Last successfully uploaded attachment, or null. */
|
|
284
|
+
attachment: Attachment | null;
|
|
285
|
+
uploading: boolean;
|
|
286
|
+
error: Error | null;
|
|
287
|
+
/**
|
|
288
|
+
* Run the upload pipeline (presigned-URL request + PUT). Updates
|
|
289
|
+
* local state with the resolved attachment on success; sets `error`
|
|
290
|
+
* and rethrows on failure so callers can surface a toast.
|
|
291
|
+
*/
|
|
292
|
+
upload: (input: AttachmentUploadInput) => Promise<Attachment>;
|
|
293
|
+
/** Reset state (clears `attachment`, `error`). */
|
|
294
|
+
reset: () => void;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Stateful wrapper around `chat.attachments.upload`. Provides
|
|
298
|
+
* `uploading` + `error` flags for the common "show a spinner while
|
|
299
|
+
* the PUT is in flight" UI, plus retention of the last attachment
|
|
300
|
+
* so the caller can reference its id when sending a message.
|
|
301
|
+
*
|
|
302
|
+
* ```tsx
|
|
303
|
+
* const { upload, uploading, attachment } = useAttachmentUpload();
|
|
304
|
+
* const onPick = async (file: File) => {
|
|
305
|
+
* const att = await upload({
|
|
306
|
+
* body: file, contentType: file.type, byteSize: file.size, filename: file.name
|
|
307
|
+
* });
|
|
308
|
+
* await chat.conversations.one(convId).messages.send({
|
|
309
|
+
* body: 'attached',
|
|
310
|
+
* custom_data: { attachment_id: att.id },
|
|
311
|
+
* });
|
|
312
|
+
* };
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
declare function useAttachmentUpload(): UseAttachmentUploadState;
|
|
316
|
+
|
|
317
|
+
interface UseAttachmentUrlState {
|
|
318
|
+
/** Presigned GET URL, or null while loading / on error. */
|
|
319
|
+
url: string | null;
|
|
320
|
+
loading: boolean;
|
|
321
|
+
error: Error | null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolve an attachment's presigned download URL. Pass `null` to
|
|
325
|
+
* skip the fetch — useful in routing-driven views where the id may
|
|
326
|
+
* not be known yet.
|
|
327
|
+
*
|
|
328
|
+
* Doesn't auto-refresh: the server's URL TTL is ~1 hour and most
|
|
329
|
+
* UI lifetimes are much shorter. If you do need a refresh (long-lived
|
|
330
|
+
* dashboard, etc.), remount the hook or wrap it in a parent that
|
|
331
|
+
* re-keys on a timer.
|
|
332
|
+
*
|
|
333
|
+
* ```tsx
|
|
334
|
+
* const { url } = useAttachmentUrl(message.custom_data?.attachment_id ?? null);
|
|
335
|
+
* return url ? <img src={url} /> : <Spinner />;
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
declare function useAttachmentUrl(attachmentId: Uuid | null): UseAttachmentUrlState;
|
|
339
|
+
|
|
340
|
+
export { PoolseProvider, type PoolseProviderProps, type ReactionMap, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping };
|