@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.
@@ -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 };