import { Channel, Socket } from 'phoenix'; type Uuid = string; type IsoDateTime = string; interface Me { id: Uuid; tenant_id: Uuid; external_id: string; display_name: string | null; custom_data: Record; is_blocked: boolean; inserted_at: IsoDateTime; updated_at: IsoDateTime; } type ConversationType = 'direct' | 'group'; interface Conversation { id: Uuid; tenant_id: Uuid; type: ConversationType; /** Display name (for group chats); null for unnamed conversations. */ name: string | null; /** Public avatar URL; null when none uploaded. */ avatar_url: string | null; /** User that created the conversation; null for system-created rows. */ created_by_user_id: Uuid | null; /** Hard cap on memberships; null = unlimited (tenant default). */ member_limit: number | null; /** * Customer-supplied free-form data β€” anything you want to attach to * the conversation that doesn't fit the schema (UI flags, tags, etc). * Server treats it as opaque JSON. */ custom_data: Record; /** * Server-defined behavioral knobs (notification rules, retention, * etc). Defaults to `{}` and is updateable. */ settings: Record; /** Most recent message's `inserted_at`; null until first message. */ last_message_at: IsoDateTime | null; /** * Plaintext preview of the most recent message body, truncated to * 80 chars. Updated server-side by `send_message`. Null when the * conversation has no messages, or when the last message had no * body (in which case `"πŸ“Ž Attachment"` etc is returned). * * Note: stored unencrypted on the server (deliberate denorm so * the conversation list doesn't pay a per-row decrypt). Apps with * full at-rest-encryption requirements should ignore this field * and render their own preview client-side. */ last_message_preview: string | null; /** Monotonic per-conversation sequence counter (last message's `sequence`). */ last_sequence: number; /** * Number of messages the caller hasn't read yet * (`last_sequence - sequence_of(my_last_read_message_id)`). * Populated only by `chat.conversations.list()` β€” undefined when the * conversation is fetched via other paths. */ unread_count?: number; /** * Tenant `external_id`s of every member, in no particular order. * Populated by code paths that preload the memberships * (`chat.conversations.list()`); may be `[]` on responses where * memberships weren't preloaded (webhook payloads, some create * responses). Use this to render the other-party name on a direct * conversation row without a separate `useMembers` call. */ member_external_ids?: string[]; inserted_at: IsoDateTime; updated_at: IsoDateTime; } interface ConversationList { data: Conversation[]; } interface ConversationCreateRequest { type: ConversationType; name?: string | null; avatar_url?: string | null; member_limit?: number | null; /** * Customer-side user IDs to add as members on creation β€” saves a * round-trip vs creating then calling `addMembers`. */ member_external_ids?: string[]; custom_data?: Record; settings?: Record; } interface ConversationUpdateRequest { name?: string | null; avatar_url?: string | null; member_limit?: number | null; custom_data?: Record; settings?: Record; } type MemberRole = 'owner' | 'admin' | 'member'; interface Membership { id: Uuid; conversation_id: Uuid; /** Internal poolse user id. Most consumer code uses `external_id` instead. */ user_id: Uuid; /** * The tenant's own user identifier (the same string you pass as * `external_id` when minting JWTs or referencing users in * `member_external_ids`). Every user reference on the wire carries * this so the SDK's `userResolver(externalId)` is a complete API * β€” no need to maintain a `poolse_user_id` mapping on your side. */ external_id: string; role: MemberRole; last_read_message_id: Uuid | null; last_read_sequence: number | null; last_read_at: IsoDateTime | null; inserted_at: IsoDateTime; updated_at: IsoDateTime; } interface MembershipList { data: Membership[]; } /** * Server accepts a batch of `external_ids` (the customer's stable user * identifiers β€” what was passed to `POST /v1/users`). The server * resolves each to its internal user_id and creates a membership row * per external_id, all in one round-trip. Optional `role` defaults to * `"member"` server-side. * * Most callers use the higher-level * `chat.conversations.one(id).addMember(externalId)` / * `addMembers([externalIds])` methods which build this shape for you. */ interface MembershipCreateRequest { external_ids: string[]; role?: MemberRole; } type MessageType = 'text' | 'system' | 'custom'; interface Message { id: Uuid; tenant_id: Uuid; conversation_id: Uuid; /** Internal poolse user id of the sender. Null for system messages. */ sender_id: Uuid | null; /** * The tenant's own user identifier for the sender β€” passed straight * to `userResolver(externalId)` to render display name + avatar. * Null for system messages, and null on read paths that didn't * preload the sender (very rare; every client-facing path preloads). */ sender_external_id: string | null; type: MessageType; body: string | null; reply_to_id: Uuid | null; thread_root_id: Uuid | null; /** * Number of replies in the thread rooted at this message. Server * populates on REST list + initial fetch; defaults to 0 for new * messages broadcast over realtime. SDK increments locally when a * new reply lands so the thread pill ("πŸ’¬ N replies") updates live. */ reply_count?: number; /** * Quote reply (WhatsApp-style): id of the message being quoted. * Independent of `reply_to_id` (which drives thread promotion). * Quoted replies stay in the main feed, NOT in a thread side-pane. */ quoted_message_id?: Uuid | null; /** * Trimmed preview of the quoted message β€” server preloads on * REST list + realtime broadcast so the SDK can render the inline * quote card without a per-message lookup. `body` is truncated * to ~200 chars; null when the quoted message was deleted post-quote. */ quoted_message?: QuotedMessagePreview | null; mentions: Uuid[]; reactions: Record; /** * Attachments linked to this message. Server populates on send + * realtime broadcast; absent on partial responses (e.g. when the * client posted with attachment_ids but the server has yet to * resolve them). */ attachments?: Attachment[]; edited_at: IsoDateTime | null; deleted_at: IsoDateTime | null; sequence: number; inserted_at: IsoDateTime; updated_at: IsoDateTime; } /** * Customer-supplied user metadata β€” the SDK doesn't know your users' * names or where their avatars live. Customers wire a * `PoolseConfig.userResolver` that maps a poolse `Uuid` to whatever * their app already stores: a display name, an avatar URL. * * The SDK caches resolved profiles in-memory (deduplicating * concurrent lookups) so a 50-message render only fires one * resolver call per unique sender. */ interface PoolseUserProfile { /** * The name shown in sender labels, mention dropdowns, member * lists, and read-receipt tooltips. Customers usually pass their * app's `display_name` / `username` / `full_name`. */ displayName: string; /** * Square avatar URL (any size β€” the UI scales). Null = render the * fallback `` with initials. Optional so customers that * don't track avatars can omit the field. */ avatarUrl?: string | null; } /** * Compact preview of a quoted message, embedded on the quoting * message when `Message.quoted_message_id` is set. Just enough for * the UI to render the inline quote card. */ interface QuotedMessagePreview { id: Uuid; sender_id: Uuid | null; /** Same external-id story as `Message.sender_external_id`. */ sender_external_id: string | null; /** Truncated to ~200 chars server-side; null when the original was deleted. */ body: string | null; deleted_at: IsoDateTime | null; inserted_at: IsoDateTime; } interface MessageList { data: Message[]; } interface MessageCreateRequest { /** * Plain-text body. Optional only when `attachment_ids` is non-empty * β€” a message with attachments AND no body is valid (just the * attachment renders). At least one of `body` or `attachment_ids` * must be present. */ body?: string | null; type?: MessageType; reply_to_id?: Uuid; /** * Quote-reply target. The new message keeps `thread_root_id` null * (no thread promotion) and is delivered alongside other top-level * messages, with a preview card pointing at this id. Must reference * a message in the same conversation. */ quoted_message_id?: Uuid; mentions?: Uuid[]; /** * Attach previously-uploaded attachments to this message. Each id * must come from `chat.attachments.upload(...)` or the lower-level * `requestUpload(...)` flow and must belong to the same tenant. * The server links them in the same transaction as the insert, so * the broadcast and REST response both carry the resolved * `Attachment` rows in `message.attachments`. */ attachment_ids?: Uuid[]; /** * Client-supplied UUID for retry-safe sends. The SDK fills this in * automatically when not provided so the optimistic temp row and * the canonical server row share a single id (used for id-based * dedup in `useMessages`). */ id?: Uuid; } interface MessageUpdateRequest { body: string; } interface ReadRequest { message_id: Uuid; } interface ReactionRequest { emoji: string; } type AttachmentStatus = 'pending' | 'ready'; interface Attachment { id: Uuid; tenant_id: Uuid; /** Linked message id; null while the attachment is still `:pending`. */ message_id: Uuid | null; /** Uploader; null for system-created attachments. */ sender_id: Uuid | null; content_type: string; byte_size: number; /** Server-computed SHA-256 of the uploaded bytes; populated after the PUT completes. */ sha256: string | null; original_filename: string | null; status: AttachmentStatus; inserted_at: IsoDateTime; updated_at: IsoDateTime; } interface AttachmentUploadRequest { content_type: string; byte_size: number; original_filename?: string; } /** Presigned PUT payload returned by `POST /v1/attachments/upload-url`. */ interface AttachmentUploadResponse { attachment: Attachment; upload: { url: string; method: 'put'; /** Headers the client MUST include on the PUT (signed into the URL). */ headers: Record; }; } /** Presigned GET payload returned by `GET /v1/attachments/:id/download-url`. */ interface AttachmentDownloadResponse { url: string; method: 'get'; } interface ErrorEnvelope { error: { code: string; message: string; doc_url: string; details?: Record; }; } /** * Hosted poolse API URL. Used as the default for `PoolseConfig.apiUrl` * when you don't pass one β€” appropriate for the vast majority of * integrations that target the official poolse cloud. Self-hosted / * staging deployments override via the `apiUrl` field. */ declare const POOLSE_API_URL = "https://api.poolse.dev"; /** * SDK configuration passed to `new Poolse(config)`. */ interface PoolseConfig { /** * Base URL of the poolse REST API. Defaults to the hosted endpoint * at `https://api.poolse.dev`. Override only for self-hosted / * staging deployments. MUST NOT include the `/v1` path β€” the SDK * adds that itself. */ apiUrl?: string; /** * Async hook the SDK calls every time it needs an `Authorization: * Bearer ` header. Most apps refresh the JWT from their own * backend here β€” the SDK never talks to poolse's `POST * /v1/users/:user_id/tokens` itself (that endpoint is API-key-authed * and lives on the Customer's BACKEND, not the End User's device). * * Return `null` to deliberately make an unauthenticated request β€” the * server will reject it, but the SDK won't error inside `getToken`. */ getToken: () => Promise | string | null; /** * Optional fetch override. Browsers and Node 22+ both ship a global * `fetch`, but tests can inject a mock here; bundlers in restricted * environments can supply a polyfill. */ fetch?: typeof globalThis.fetch; /** * Retry budget for transient failures (network + 5xx + 429). Defaults * to 3 attempts after the initial request. Set to 0 to disable. */ maxRetries?: number; /** * Base for the exponential backoff, in milliseconds. Each retry waits * `min(maxBackoffMs, baseBackoffMs * 2^attempt)` plus jitter, OR honours * the `Retry-After` header if present. Default 250 ms. */ baseBackoffMs?: number; /** Hard cap on a single retry delay. Default 30_000 ms. */ maxBackoffMs?: number; /** * Override the idempotency-key generator. Defaults to * `crypto.randomUUID()`. Most apps don't need to override this β€” the * generator is exposed mainly for deterministic tests. */ generateIdempotencyKey?: () => string; /** * Override the WebSocket URL. Defaults to `apiUrl` with `http(s)://` * swapped to `ws(s)://`, suitable when the realtime gateway shares * its origin with the REST API. Set explicitly for split-host * deployments (`https://api.example.com` REST + `wss://realtime.example.com` WS). */ wsUrl?: string; /** * Path the WebSocket is mounted on. Defaults to `/socket` β€” matches * `CaasRealtimeWeb.UserSocket`'s mount point. */ socketPath?: string; /** * Called when the underlying socket encounters a non-fatal error * (Phoenix retries internally). Useful for surfacing reconnect * banners in the UI without coupling to socket internals. */ onSocketError?: (err: Error) => void; /** * Resolve the tenant's user identifier (`external_id` β€” same string * you pass when minting JWTs and referencing users in * `member_external_ids`) to the customer's own user metadata * (display name + avatar). Called by `chat.users.get(externalId)` * and the `useUser(externalId)` React hook whenever a UI component * needs to render a participant. * * The SDK caches results in-memory and dedupes concurrent calls, * so a busy chat with 50 messages from 5 senders fires the * resolver 5 times β€” once per unique sender β€” not 50. * * Customers hit their OWN backend / store here, keyed by **their own * user id** (no poolse uuid mapping required): * * userResolver: async (externalId) => { * const u = await fetch(`/api/users/${externalId}`).then((r) => r.json()); * return { displayName: u.full_name, avatarUrl: u.avatar_url }; * } * * Sync returns are fine when the data's already in memory: * * userResolver: (externalId) => directory[externalId] ?? null * * Return `null` when the user can't be found β€” components fall back * to the external_id as a label and an initials avatar. */ userResolver?: (externalId: string) => Promise | PoolseUserProfile | null; } /** Internal resolved config β€” all the defaults filled in. */ interface ResolvedConfig { apiUrl: string; getToken: PoolseConfig['getToken']; fetch: typeof globalThis.fetch; maxRetries: number; baseBackoffMs: number; maxBackoffMs: number; generateIdempotencyKey: () => string; wsUrl: string | undefined; socketPath: string; onSocketError: ((err: Error) => void) | undefined; userResolver: PoolseConfig['userResolver']; } /** * Voice types shared by the room (Discord-style "join voice") and call * (WhatsApp-style ringing) surfaces. * * The WebRTC pieces are declared structurally rather than as the DOM's * concrete `RTCPeerConnection`, so React Native can hand us * `react-native-webrtc` β€” API-compatible, different classes β€” through * {@link WebRtcAdapter} without the SDK importing anything native. */ /** Connection lifecycle for a voice room. */ type VoiceStatus = 'idle' | 'connecting' | 'connected' | 'error'; /** Someone currently in a voice room, including yourself. */ interface VoiceParticipant { userId: string; muted: boolean; speaking: boolean; /** ms epoch from presence metadata; stable ordering key for rosters. */ joinedAt: number; /** True for the local participant. */ isSelf: boolean; } /** The subset of `MediaStreamTrack` the SDK touches. */ interface VoiceTrack { kind: string; enabled: boolean; stop(): void; } /** The subset of `MediaStream` the SDK touches. */ interface VoiceStream { id: string; getTracks(): VoiceTrack[]; getAudioTracks(): VoiceTrack[]; } /** Session description exchanged during negotiation. */ interface VoiceDescription { type: string; sdp?: string | undefined; } /** ICE candidate in its wire (JSON) form. */ interface VoiceCandidate { candidate?: string | undefined; sdpMid?: string | null | undefined; sdpMLineIndex?: number | null | undefined; usernameFragment?: string | null | undefined; } /** The subset of `RTCPeerConnection` the SDK drives. */ interface VoicePeerConnection { addTrack(track: VoiceTrack, stream: VoiceStream): unknown; createOffer(): Promise; createAnswer(): Promise; setLocalDescription(description: VoiceDescription): Promise; setRemoteDescription(description: VoiceDescription): Promise; addIceCandidate(candidate: VoiceCandidate): Promise; close(): void; readonly remoteDescription: VoiceDescription | null; onicecandidate: ((event: { candidate: VoiceCandidate | null; }) => void) | null; ontrack: ((event: { streams: readonly VoiceStream[]; }) => void) | null; onconnectionstatechange: (() => void) | null; readonly connectionState: string; } /** * Platform binding for WebRTC. The browser default is built in; React * Native supplies one backed by `react-native-webrtc`. */ interface WebRtcAdapter { createPeerConnection(iceServers: VoiceIceServer[]): VoicePeerConnection; getUserMedia(): Promise; /** * Attach a remote stream to a playback sink. The browser adapter * creates a hidden `