@antzsoft/chat-core 1.0.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/README.md ADDED
@@ -0,0 +1,1508 @@
1
+ # @antzsoft/chat-core
2
+
3
+ Platform-agnostic TypeScript core for Antz Chat — API client, Socket.IO wrapper, Zustand stores, and a headless client class that works in browser, React Native (Expo or bare), and Node.js.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@antzsoft/chat-core)](https://www.npmjs.com/package/@antzsoft/chat-core)
6
+ [![license](https://img.shields.io/npm/l/@antzsoft/chat-core)](./LICENSE)
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ `@antzsoft/chat-core` provides the shared foundation that both `@antzsoft/chat-web-sdk` and `@antzsoft/chat-rn-sdk` are built on. You can also use it directly when you need headless control — bot integrations, custom UIs, server-side tooling, or any environment that doesn't match the higher-level SDK's assumptions.
13
+
14
+ **Key capabilities:**
15
+
16
+ - Axios HTTP client with automatic token injection, refresh handling, and multi-tenant headers
17
+ - Socket.IO wrapper with typed event emitters, ack-based operations, and connection-state management
18
+ - Zustand auth store with platform-portable token persistence
19
+ - Zustand chat store for UI state (active conversation, typing indicators, online presence, replies)
20
+ - `AntzChatClient` — a single class that wires everything together for headless use
21
+ - Full TypeScript types for all entities, API payloads, and socket events
22
+
23
+ The package ships both ESM (`dist/index.js`) and CJS (`dist/index.cjs`) builds, plus `.d.ts` declarations.
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @antzsoft/chat-core
31
+ ```
32
+
33
+ `axios`, `socket.io-client`, and `zustand` are regular dependencies — they are bundled in the package and require no separate install. There are no peer dependencies.
34
+
35
+ ---
36
+
37
+ ## Using This SDK Independently (Custom UI)
38
+
39
+ `@antzsoft/chat-core` is a **fully standalone SDK**. You do not need `@antzsoft/chat-web-sdk` or `@antzsoft/chat-rn-sdk` to build a complete chat app — those packages only add pre-built UI components. This package gives you everything needed to build your own UI on any platform.
40
+
41
+ ### What you get out of the box
42
+
43
+ | Capability | What the SDK provides |
44
+ |---|---|
45
+ | Authentication | Login, register, logout, token refresh (automatic on 401) |
46
+ | Conversations | List, create (group/DM), update, delete, mute, pin, leave, manage members |
47
+ | Messages | Send, edit, delete, react, star, pin, search, paginate |
48
+ | File uploads | Presigned URL pipeline — request URL → upload binary → confirm |
49
+ | Real-time | Socket.IO wrapper — send/receive messages, typing, read receipts, presence |
50
+ | State management | Zustand auth store (persisted) + chat store (typing users, online status, reply/edit state) |
51
+ | Push notifications | Device token registration/removal API |
52
+ | TypeScript | Full types for every entity, API payload, and socket event |
53
+
54
+ ### What you must provide
55
+
56
+ The SDK has two required adapters that differ between platforms. You write them once — they are simple wrappers:
57
+
58
+ #### 1. `persistStorage` — token persistence
59
+
60
+ The SDK stores auth tokens under the key `"antz-chat-auth"` using this adapter. Tokens survive page reloads (web) or app restarts (RN) if you point it at a persistent store.
61
+
62
+ **Why it's required:** The SDK is platform-agnostic — it can't assume `localStorage` exists (Node.js, RN) or `AsyncStorage` exists (web, Node.js). You tell it where to store tokens.
63
+
64
+ ```typescript
65
+ // Browser — localStorage
66
+ const persistStorage = {
67
+ getItem: (key: string) => localStorage.getItem(key),
68
+ setItem: (key: string, value: string) => localStorage.setItem(key, value),
69
+ removeItem: (key: string) => localStorage.removeItem(key),
70
+ };
71
+
72
+ // React Native — AsyncStorage
73
+ import AsyncStorage from '@react-native-async-storage/async-storage';
74
+ const persistStorage = {
75
+ getItem: (key: string) => AsyncStorage.getItem(key),
76
+ setItem: (key: string, value: string) => AsyncStorage.setItem(key, value),
77
+ removeItem: (key: string) => AsyncStorage.removeItem(key),
78
+ };
79
+
80
+ // Node.js / server — in-memory (tokens lost on restart, fine for bots)
81
+ const _store: Record<string, string> = {};
82
+ const persistStorage = {
83
+ getItem: (key: string) => _store[key] ?? null,
84
+ setItem: (key: string, value: string) => { _store[key] = value; },
85
+ removeItem: (key: string) => { delete _store[key]; },
86
+ };
87
+ ```
88
+
89
+ #### 2. `platformUploadFn` — binary file upload
90
+
91
+ The SDK handles the full upload pipeline (requesting presigned URLs, confirming uploads) but delegates the actual binary transfer to this function. This is because the HTTP APIs differ between platforms — XHR on web, fetch/FileSystem on RN, fs on Node.js.
92
+
93
+ **Why it's required:** Sending binary data to S3/GCS presigned URLs works differently on each platform. You provide the right implementation for your environment.
94
+
95
+ ```typescript
96
+ // Browser — XHR with progress reporting
97
+ const platformUploadFn = async (presigned, file, onProgress) => {
98
+ await new Promise<void>((resolve, reject) => {
99
+ const xhr = new XMLHttpRequest();
100
+ xhr.open(presigned.method, presigned.uploadUrl);
101
+ Object.entries(presigned.headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
102
+ xhr.upload.onprogress = (e) => onProgress?.(e.loaded / e.total);
103
+ xhr.onload = () => xhr.status < 400 ? resolve() : reject(new Error(`${xhr.status}`));
104
+ xhr.onerror = () => reject(new Error('Network error'));
105
+ if (presigned.method === 'PUT') {
106
+ fetch(file.uri).then(r => r.blob()).then(blob => xhr.send(blob));
107
+ } else {
108
+ const fd = new FormData();
109
+ Object.entries(presigned.fields ?? {}).forEach(([k, v]) => fd.append(k, v));
110
+ fetch(file.uri).then(r => r.blob()).then(blob => { fd.append('file', blob, file.name); xhr.send(fd); });
111
+ }
112
+ });
113
+ };
114
+
115
+ // React Native — fetch (works on Expo and bare RN)
116
+ const platformUploadFn = async (presigned, file, onProgress) => {
117
+ onProgress?.(0);
118
+ const res = await fetch(presigned.uploadUrl, {
119
+ method: presigned.method,
120
+ headers: presigned.headers,
121
+ body: { uri: file.uri, name: file.name, type: file.type } as any,
122
+ });
123
+ if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
124
+ onProgress?.(1);
125
+ };
126
+
127
+ // Node.js — fs + fetch (Node 18+)
128
+ import { readFileSync } from 'fs';
129
+ const platformUploadFn = async (presigned, file) => {
130
+ const body = readFileSync(file.uri.replace('file://', ''));
131
+ const res = await fetch(presigned.uploadUrl, {
132
+ method: presigned.method,
133
+ headers: presigned.headers,
134
+ body,
135
+ });
136
+ if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
137
+ };
138
+ ```
139
+
140
+ > **Note:** If your app does not use file uploads at all, you can pass a no-op:
141
+ > ```typescript
142
+ > const platformUploadFn = async () => {};
143
+ > ```
144
+
145
+ ### Minimum setup — 5 lines
146
+
147
+ ```typescript
148
+ import { AntzChatClient } from '@antzsoft/chat-core';
149
+
150
+ const client = new AntzChatClient({
151
+ apiUrl: 'https://your-server.com/api/v1',
152
+ persistStorage, // your adapter from above
153
+ platformUploadFn, // your adapter from above
154
+ });
155
+
156
+ await client.auth.login({ email: 'user@example.com', password: 'secret' });
157
+ await client.connect(); // opens socket
158
+
159
+ client.socket.on('new_message', (evt) => console.log(evt.message));
160
+ client.socket.emit.joinRoom('your-conversation-id');
161
+ ```
162
+
163
+ ### Authentication options
164
+
165
+ ```typescript
166
+ // Option 1 — SDK manages login/logout (built-in auth)
167
+ await client.auth.login({ email, password });
168
+
169
+ // Option 2 — pre-authenticated token from your own auth system
170
+ const client = new AntzChatClient({ apiUrl, persistStorage, platformUploadFn, authToken: 'eyJ...' });
171
+
172
+ // Option 3 — dynamic token provider (SSO, token rotation)
173
+ const client = new AntzChatClient({
174
+ apiUrl, persistStorage, platformUploadFn,
175
+ authProvider: async () => {
176
+ const token = await yourApp.getAccessToken(); // your auth library
177
+ return token;
178
+ },
179
+ });
180
+ ```
181
+
182
+ ### What you build yourself
183
+
184
+ When using core SDK directly, you are responsible for building:
185
+ - Your own UI components (conversation list, message bubbles, input box, etc.)
186
+ - Wiring socket events to your UI state (the `useChatStore` Zustand store helps with this)
187
+ - File picker integration (to produce `UploadableFile` objects for `uploadFiles`)
188
+
189
+ The rest of this README documents all the APIs, stores, types, and socket events in full detail.
190
+
191
+ ---
192
+
193
+ ## Quick Start
194
+
195
+ The fastest way to go headless: instantiate `AntzChatClient`, connect, and start listening.
196
+
197
+ ```typescript
198
+ import {
199
+ AntzChatClient,
200
+ type AntzChatConfig,
201
+ type NewMessageEvent,
202
+ } from '@antzsoft/chat-core';
203
+
204
+ // Minimal localStorage adapter (browser)
205
+ const localStorageAdapter = {
206
+ getItem: (key: string) => localStorage.getItem(key),
207
+ setItem: (key: string, value: string) => localStorage.setItem(key, value),
208
+ removeItem: (key: string) => localStorage.removeItem(key),
209
+ };
210
+
211
+ // Platform upload function (browser — XHR with progress)
212
+ const platformUploadFn = async (presigned, file, onProgress) => {
213
+ await new Promise<void>((resolve, reject) => {
214
+ const xhr = new XMLHttpRequest();
215
+ xhr.open(presigned.method, presigned.uploadUrl);
216
+ Object.entries(presigned.headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
217
+ xhr.upload.onprogress = (e) => onProgress?.(e.loaded / e.total);
218
+ xhr.onload = () => (xhr.status < 400 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)));
219
+ xhr.onerror = () => reject(new Error('Network error during upload'));
220
+ xhr.send(file.uri ? null : (file as any)); // blob or body per presigned.method
221
+ });
222
+ };
223
+
224
+ const config: AntzChatConfig = {
225
+ apiUrl: 'https://api.yourapp.com/api/v1',
226
+ persistStorage: localStorageAdapter,
227
+ platformUploadFn,
228
+ };
229
+
230
+ const client = new AntzChatClient(config);
231
+
232
+ // Log in (stores tokens automatically)
233
+ const { user, tokens } = await client.auth.login({ email: 'user@example.com', password: 'secret' });
234
+ console.log('Logged in as', user.displayName);
235
+
236
+ // Open a socket connection
237
+ await client.connect();
238
+
239
+ // Listen for incoming messages
240
+ client.socket.on('new_message', (event: NewMessageEvent) => {
241
+ console.log('[new message]', event.message.content.text);
242
+ });
243
+
244
+ // Join a conversation room
245
+ client.socket.emit.joinRoom('conv-123');
246
+
247
+ // Send a message over the socket
248
+ await client.socket.emit.sendMessage({
249
+ conversationId: 'conv-123',
250
+ text: 'Hello!',
251
+ tempId: crypto.randomUUID(),
252
+ });
253
+
254
+ // Or use the REST API directly
255
+ const history = await client.messages.list('conv-123', { limit: 50 });
256
+ console.log('Loaded', history.data.length, 'messages');
257
+
258
+ // Clean up
259
+ client.disconnect();
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Configuration
265
+
266
+ ### `AntzChatConfig`
267
+
268
+ ```typescript
269
+ interface AntzChatConfig {
270
+ /**
271
+ * Base URL for all REST API requests.
272
+ * Include the versioned path segment — e.g. "https://api.yourapp.com/api/v1".
273
+ * Required.
274
+ */
275
+ apiUrl: string;
276
+
277
+ /**
278
+ * Platform-specific function that performs the actual binary upload
279
+ * to a presigned URL. The core never touches binary data directly —
280
+ * it delegates to this function so the same codebase works on web and RN.
281
+ * Required.
282
+ */
283
+ platformUploadFn: PlatformUploadFn;
284
+
285
+ /**
286
+ * Key-value storage adapter for auth token persistence.
287
+ * Web: wrap localStorage. RN: wrap AsyncStorage.
288
+ * Required.
289
+ */
290
+ persistStorage: PersistStorage;
291
+
292
+ /**
293
+ * WebSocket server URL. Defaults to apiUrl with any "/api/vN" suffix stripped.
294
+ * The socket connects to "{socketUrl}/chat".
295
+ * Optional.
296
+ */
297
+ socketUrl?: string;
298
+
299
+ /**
300
+ * Static JWT. Pass when you have a token from outside the SDK
301
+ * (e.g. SSO flow completed by the host app). Skips the login step.
302
+ * Use this OR authProvider, not both.
303
+ * Optional.
304
+ */
305
+ authToken?: string;
306
+
307
+ /**
308
+ * Async function that returns a fresh access token. Called before every
309
+ * request and on socket reconnect. Preferred when the host app manages
310
+ * its own auth lifecycle.
311
+ * Optional.
312
+ */
313
+ authProvider?: () => Promise<string>;
314
+
315
+ /**
316
+ * Tenant identifier for multi-tenant backends.
317
+ * Sent as the "X-Tenant-ID" request header when provided.
318
+ * Optional.
319
+ */
320
+ tenantId?: string;
321
+
322
+ /**
323
+ * Encryption mode. Must match the server's ENCRYPTION_MODE env var.
324
+ * Default: 'none'.
325
+ */
326
+ encryptionMode?: 'none' | 'server';
327
+
328
+ /**
329
+ * Fine-grained upload constraints and callbacks.
330
+ * Optional — sensible defaults are applied for all sub-fields.
331
+ */
332
+ upload?: UploadConfig;
333
+ }
334
+ ```
335
+
336
+ ### `PersistStorage`
337
+
338
+ Supports both synchronous (localStorage) and asynchronous (AsyncStorage) storage backends.
339
+
340
+ ```typescript
341
+ interface PersistStorage {
342
+ getItem(key: string): string | null | Promise<string | null>;
343
+ setItem(key: string, value: string): void | Promise<void>;
344
+ removeItem(key: string): void | Promise<void>;
345
+ }
346
+ ```
347
+
348
+ **Web (localStorage):**
349
+
350
+ ```typescript
351
+ const persistStorage: PersistStorage = {
352
+ getItem: (key) => localStorage.getItem(key),
353
+ setItem: (key, value) => localStorage.setItem(key, value),
354
+ removeItem: (key) => localStorage.removeItem(key),
355
+ };
356
+ ```
357
+
358
+ **React Native (AsyncStorage):**
359
+
360
+ ```typescript
361
+ import AsyncStorage from '@react-native-async-storage/async-storage';
362
+
363
+ const persistStorage: PersistStorage = {
364
+ getItem: (key) => AsyncStorage.getItem(key),
365
+ setItem: (key, value) => AsyncStorage.setItem(key, value),
366
+ removeItem: (key) => AsyncStorage.removeItem(key),
367
+ };
368
+ ```
369
+
370
+ ### `PlatformUploadFn`
371
+
372
+ The core requests a presigned URL from the server, then hands the presigned response and the local file descriptor to this function. The function is responsible for the actual HTTP upload and for calling `onProgress` with a 0–1 fraction.
373
+
374
+ ```typescript
375
+ type PlatformUploadFn = (
376
+ presigned: PresignedUrlResponse,
377
+ file: UploadableFile,
378
+ onProgress?: (pct: number) => void,
379
+ ) => Promise<void>;
380
+ ```
381
+
382
+ **Web implementation (XHR):**
383
+
384
+ ```typescript
385
+ const platformUploadFn: PlatformUploadFn = (presigned, file, onProgress) =>
386
+ new Promise((resolve, reject) => {
387
+ const xhr = new XMLHttpRequest();
388
+ xhr.open(presigned.method, presigned.uploadUrl);
389
+ Object.entries(presigned.headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
390
+ xhr.upload.onprogress = (e) => onProgress?.(e.loaded / e.total);
391
+ xhr.onload = () => (xhr.status < 400 ? resolve() : reject(new Error(`${xhr.status}`)));
392
+ xhr.onerror = () => reject(new Error('Network error'));
393
+
394
+ // For PUT: send the raw blob. For POST (S3-style): send FormData with fields.
395
+ if (presigned.method === 'PUT') {
396
+ fetch(file.uri)
397
+ .then((r) => r.blob())
398
+ .then((blob) => xhr.send(blob));
399
+ } else {
400
+ const fd = new FormData();
401
+ Object.entries(presigned.fields ?? {}).forEach(([k, v]) => fd.append(k, v));
402
+ fetch(file.uri)
403
+ .then((r) => r.blob())
404
+ .then((blob) => { fd.append('file', blob, file.name); xhr.send(fd); });
405
+ }
406
+ });
407
+ ```
408
+
409
+ **React Native implementation (fetch):**
410
+
411
+ ```typescript
412
+ import * as FileSystem from 'expo-file-system';
413
+
414
+ const platformUploadFn: PlatformUploadFn = async (presigned, file, onProgress) => {
415
+ const callback = (progress: FileSystem.UploadProgressData) => {
416
+ onProgress?.(progress.totalBytesSent / progress.totalBytesExpectedToSend);
417
+ };
418
+
419
+ const uploadTask = FileSystem.createUploadTask(
420
+ presigned.uploadUrl,
421
+ file.uri,
422
+ {
423
+ httpMethod: presigned.method,
424
+ headers: presigned.headers,
425
+ uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
426
+ },
427
+ callback,
428
+ );
429
+
430
+ const result = await uploadTask.uploadAsync();
431
+ if (!result || result.status >= 400) {
432
+ throw new Error(`Upload failed: ${result?.status}`);
433
+ }
434
+ };
435
+ ```
436
+
437
+ ### `UploadConfig`
438
+
439
+ ```typescript
440
+ interface UploadConfig {
441
+ /**
442
+ * File size limits in MB. Pass a single number to apply uniformly,
443
+ * or per-type limits. Defaults: image 5, video 25, audio 10, document 10.
444
+ */
445
+ maxFileSizeMB?: number | {
446
+ image?: number;
447
+ video?: number;
448
+ audio?: number;
449
+ document?: number;
450
+ default?: number;
451
+ };
452
+
453
+ /** Max attachments per message. Default: 10. */
454
+ maxFilesPerMessage?: number;
455
+
456
+ /** Restrict which file categories are allowed. Default: all four. */
457
+ allowedTypes?: Array<'image' | 'video' | 'audio' | 'document'>;
458
+
459
+ /** Called when a file fails local validation or the upload itself fails. */
460
+ onUploadError?: (file: UploadableFile, error: Error) => void;
461
+
462
+ /** Called with 0–100 aggregate progress during a batch upload. */
463
+ onProgress?: (progress: number) => void;
464
+ }
465
+ ```
466
+
467
+ **Example with per-type limits:**
468
+
469
+ ```typescript
470
+ const config: AntzChatConfig = {
471
+ apiUrl: 'https://api.yourapp.com/api/v1',
472
+ persistStorage,
473
+ platformUploadFn,
474
+ upload: {
475
+ maxFileSizeMB: { image: 10, video: 50, audio: 20, document: 25 },
476
+ maxFilesPerMessage: 5,
477
+ allowedTypes: ['image', 'document'],
478
+ onUploadError: (file, err) => console.error(`Failed to upload ${file.name}:`, err),
479
+ onProgress: (pct) => setUploadProgress(pct),
480
+ },
481
+ };
482
+ ```
483
+
484
+ ---
485
+
486
+ ## API Reference
487
+
488
+ ### `AntzChatClient` (Headless)
489
+
490
+ A single class that initializes the API client, auth store, and socket in one shot. Use this when you need direct programmatic control and don't want to wire the internals yourself.
491
+
492
+ ```typescript
493
+ class AntzChatClient {
494
+ readonly auth: typeof authApi;
495
+ readonly messages: typeof messagesApi;
496
+ readonly conversations: typeof conversationsApi;
497
+ readonly storage: typeof storageApi;
498
+ readonly socket: {
499
+ emit: typeof socketEmit;
500
+ on(event: string, handler: (...args: unknown[]) => void): void;
501
+ off(event: string, handler: (...args: unknown[]) => void): void;
502
+ };
503
+
504
+ constructor(config: AntzChatConfig);
505
+
506
+ /** Connect the Socket.IO client. Resolves when the connection is established. */
507
+ connect(): Promise<void>;
508
+
509
+ /** Disconnect the Socket.IO client and clear the socket singleton. */
510
+ disconnect(): void;
511
+
512
+ /**
513
+ * High-level batch upload. Requests presigned URLs, delegates binary upload
514
+ * to the configured platformUploadFn, and confirms each upload with the server.
515
+ */
516
+ uploadFiles(files: UploadableFile[], conversationId?: string): Promise<BatchUploadResult>;
517
+ }
518
+ ```
519
+
520
+ **Usage:**
521
+
522
+ ```typescript
523
+ const client = new AntzChatClient(config);
524
+
525
+ // Option A — SDK manages auth
526
+ await client.auth.login({ email: 'user@example.com', password: 'secret' });
527
+
528
+ // Option B — pre-authenticated token in config
529
+ // const client = new AntzChatClient({ ...config, authToken: 'eyJ...' });
530
+
531
+ await client.connect();
532
+
533
+ // Listen to socket events
534
+ client.socket.on('new_message', (evt: NewMessageEvent) => handleMessage(evt));
535
+
536
+ // Emit socket events
537
+ client.socket.emit.joinRoom('conv-abc');
538
+ await client.socket.emit.sendMessage({ conversationId: 'conv-abc', text: 'Hi', tempId: 'tmp-1' });
539
+
540
+ // REST calls
541
+ const convs = await client.conversations.list({ page: 1, limit: 20 });
542
+ const msgs = await client.messages.list('conv-abc', { limit: 50 });
543
+
544
+ // Upload files
545
+ const result = await client.uploadFiles(
546
+ [{ uri: 'blob:http://...', name: 'photo.jpg', type: 'image/jpeg', size: 204800 }],
547
+ 'conv-abc',
548
+ );
549
+ console.log('Uploaded:', result.successful);
550
+ console.log('Failed:', result.failed);
551
+
552
+ client.disconnect();
553
+ ```
554
+
555
+ ---
556
+
557
+ ### Auth API (`authApi`)
558
+
559
+ ```typescript
560
+ import { authApi } from '@antzsoft/chat-core';
561
+ ```
562
+
563
+ | Method | Signature | Description |
564
+ |---|---|---|
565
+ | `login` | `(credentials: LoginCredentials) => Promise<AuthResponse>` | Authenticate with email + password. Returns user and tokens. |
566
+ | `register` | `(data: RegisterData) => Promise<AuthResponse>` | Create a new account. |
567
+ | `refresh` | `(refreshToken: string) => Promise<AuthTokens>` | Exchange a refresh token for new tokens. The HTTP client handles this automatically on 401 — call manually only if needed. |
568
+ | `logout` | `(refreshToken?: string) => Promise<void>` | Invalidate the current session. |
569
+ | `logoutAll` | `() => Promise<void>` | Invalidate all sessions for the current user. |
570
+ | `getMe` | `() => Promise<User>` | Fetch the current user's profile. |
571
+
572
+ ```typescript
573
+ // Login
574
+ const { user, tokens } = await authApi.login({ email: 'user@example.com', password: 'secret' });
575
+
576
+ // Register
577
+ const { user } = await authApi.register({
578
+ email: 'new@example.com',
579
+ password: 'hunter2',
580
+ username: 'newuser',
581
+ displayName: 'New User',
582
+ tenantId: 'tenant-xyz',
583
+ });
584
+
585
+ // Logout
586
+ await authApi.logout(tokens.refreshToken);
587
+ ```
588
+
589
+ ---
590
+
591
+ ### Messages API (`messagesApi`)
592
+
593
+ ```typescript
594
+ import { messagesApi } from '@antzsoft/chat-core';
595
+ ```
596
+
597
+ | Method | Signature | Description |
598
+ |---|---|---|
599
+ | `list` | `(conversationId: string, params?: ListMessagesParams) => Promise<CursorPaginatedResponse<Message>>` | Fetch messages with cursor pagination. |
600
+ | `get` | `(messageId: string) => Promise<Message>` | Fetch a single message. |
601
+ | `send` | `(conversationId: string, payload: SendData) => Promise<Message>` | Send a message via REST (use `socketEmit.sendMessage` for real-time delivery). |
602
+ | `update` | `(messageId: string, text: string) => Promise<Message>` | Edit message text. |
603
+ | `delete` | `(messageId: string) => Promise<void>` | Delete a message. |
604
+ | `addReaction` | `(messageId: string, emoji: string) => Promise<Message>` | Add an emoji reaction. |
605
+ | `removeReaction` | `(messageId: string, emoji: string) => Promise<Message>` | Remove an emoji reaction. |
606
+ | `star` | `(messageId: string) => Promise<void>` | Star a message. |
607
+ | `unstar` | `(messageId: string) => Promise<void>` | Unstar a message. |
608
+ | `getStarred` | `(params?: { page?: number; limit?: number; conversationId?: string }) => Promise<PaginatedResponse<Message>>` | List starred messages. |
609
+ | `search` | `(params: SearchParams) => Promise<PaginatedResponse<Message>>` | Full-text message search. |
610
+ | `markAsRead` | `(conversationId: string, messageId?: string) => Promise<void>` | Mark messages as read via REST. |
611
+ | `pin` | `(messageId: string) => Promise<Message>` | Pin a message. |
612
+ | `unpin` | `(messageId: string) => Promise<Message>` | Unpin a message. |
613
+ | `getPinned` | `(conversationId: string) => Promise<Message[]>` | List pinned messages in a conversation. |
614
+
615
+ ```typescript
616
+ interface ListMessagesParams {
617
+ cursor?: string; // Opaque cursor for pagination
618
+ limit?: number; // Default decided by server
619
+ direction?: 'before' | 'after';
620
+ }
621
+
622
+ interface SendData {
623
+ text?: string;
624
+ attachments?: SendMessageAttachment[];
625
+ replyTo?: string; // messageId of the message being replied to
626
+ tempId?: string; // Client-generated ID for optimistic UI
627
+ isEncrypted?: boolean;
628
+ }
629
+
630
+ interface SearchParams {
631
+ query: string;
632
+ conversationId?: string;
633
+ page?: number;
634
+ limit?: number;
635
+ }
636
+ ```
637
+
638
+ ```typescript
639
+ // Cursor-paginated message history
640
+ const page1 = await messagesApi.list('conv-abc', { limit: 50 });
641
+ if (page1.meta.hasMore && page1.meta.nextCursor) {
642
+ const page2 = await messagesApi.list('conv-abc', {
643
+ cursor: page1.meta.nextCursor,
644
+ direction: 'before',
645
+ limit: 50,
646
+ });
647
+ }
648
+
649
+ // Send with a reply reference
650
+ await messagesApi.send('conv-abc', {
651
+ text: 'Good point!',
652
+ replyTo: 'msg-456',
653
+ tempId: crypto.randomUUID(),
654
+ });
655
+
656
+ // Search
657
+ const results = await messagesApi.search({ query: 'deployment', conversationId: 'conv-abc' });
658
+ ```
659
+
660
+ ---
661
+
662
+ ### Conversations API (`conversationsApi`)
663
+
664
+ ```typescript
665
+ import { conversationsApi } from '@antzsoft/chat-core';
666
+ ```
667
+
668
+ | Method | Signature | Description |
669
+ |---|---|---|
670
+ | `list` | `(params?: { page?: number; limit?: number }) => Promise<PaginatedResponse<Conversation>>` | List all conversations the current user is part of. |
671
+ | `get` | `(conversationId: string) => Promise<Conversation>` | Fetch a single conversation. |
672
+ | `createGroup` | `(data: CreateGroupData) => Promise<Conversation>` | Create a group conversation. |
673
+ | `createDirect` | `(data: CreateDirectData) => Promise<Conversation>` | Start or retrieve a direct conversation with another user. |
674
+ | `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name, description, or icon. |
675
+ | `delete` | `(conversationId: string) => Promise<void>` | Delete a conversation (admin only). |
676
+ | `addParticipants` | `(conversationId: string, userIds: string[]) => Promise<Conversation>` | Add one or more participants. |
677
+ | `removeParticipant` | `(conversationId: string, userId: string) => Promise<Conversation>` | Remove a participant. |
678
+ | `updateParticipantRole` | `(conversationId: string, userId: string, role: 'admin' \| 'member') => Promise<Conversation>` | Promote or demote a participant. |
679
+ | `mute` | `(conversationId: string, mutedUntil?: string) => Promise<void>` | Mute notifications. Pass an ISO date string to mute until a specific time. |
680
+ | `unmute` | `(conversationId: string) => Promise<void>` | Unmute a conversation. |
681
+ | `pin` | `(conversationId: string) => Promise<void>` | Pin a conversation to the top of the list. |
682
+ | `unpin` | `(conversationId: string) => Promise<void>` | Unpin a conversation. |
683
+ | `leave` | `(conversationId: string) => Promise<void>` | Leave a group conversation. |
684
+ | `getMembers` | `(conversationId: string) => Promise<User[]>` | Fetch full user profiles for all participants. |
685
+ | `searchUsers` | `(query: string) => Promise<User[]>` | Search users by name or email (for adding to conversations). |
686
+
687
+ ```typescript
688
+ interface CreateGroupData {
689
+ name: string;
690
+ description?: string;
691
+ icon?: string; // Emoji or short string used as the group avatar
692
+ participantIds: string[];
693
+ }
694
+
695
+ interface CreateDirectData {
696
+ userId: string;
697
+ }
698
+
699
+ interface UpdateConversationData {
700
+ name?: string;
701
+ description?: string;
702
+ icon?: string;
703
+ }
704
+ ```
705
+
706
+ ```typescript
707
+ // Create a group
708
+ const group = await conversationsApi.createGroup({
709
+ name: 'Engineering',
710
+ participantIds: ['user-a', 'user-b', 'user-c'],
711
+ });
712
+
713
+ // Start a DM
714
+ const dm = await conversationsApi.createDirect({ userId: 'user-b' });
715
+
716
+ // Add members
717
+ await conversationsApi.addParticipants(group.id, ['user-d', 'user-e']);
718
+
719
+ // Mute for 8 hours
720
+ const mutedUntil = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString();
721
+ await conversationsApi.mute(group.id, mutedUntil);
722
+ ```
723
+
724
+ ---
725
+
726
+ ### Storage API (`storageApi` and `uploadBatch`)
727
+
728
+ ```typescript
729
+ import { storageApi, uploadBatch } from '@antzsoft/chat-core';
730
+ ```
731
+
732
+ **`storageApi` methods:**
733
+
734
+ | Method | Signature | Description |
735
+ |---|---|---|
736
+ | `requestPresignedUrl` | `(payload: PresignedUrlRequest) => Promise<PresignedUrlResponse>` | Request a single presigned upload URL. |
737
+ | `requestPresignedUrlBatch` | `(files: PresignedUrlRequest[]) => Promise<{ urls: PresignedUrlResponse[]; errors: Array<{ filename: string; error: string }> }>` | Batch presigned URL request. |
738
+ | `confirmUpload` | `(fileId: string) => Promise<FileResponse>` | Confirm that a binary upload is complete. Required after every upload. |
739
+ | `getFile` | `(fileId: string) => Promise<FileResponse>` | Fetch file metadata. |
740
+ | `getFileUrl` | `(fileId: string, expiresIn?: number) => Promise<{ url: string; expiresAt: string }>` | Get a fresh signed URL for an already-uploaded file. |
741
+ | `deleteFile` | `(fileId: string) => Promise<void>` | Delete a file. |
742
+ | `getConversationFiles` | `(conversationId: string, params?: { page?: number; limit?: number; type?: FileType }) => Promise<PaginatedResponse<FileResponse>>` | List all files shared in a conversation. |
743
+ | `getMyFiles` | `(params?: { page?: number; limit?: number }) => Promise<PaginatedResponse<FileResponse>>` | List files uploaded by the current user. |
744
+
745
+ **`uploadBatch` — high-level helper:**
746
+
747
+ ```typescript
748
+ function uploadBatch(
749
+ files: UploadableFile[],
750
+ platformUploadFn: PlatformUploadFn,
751
+ conversationId?: string,
752
+ onProgress?: (pct: number) => void,
753
+ ): Promise<BatchUploadResult>
754
+ ```
755
+
756
+ Handles the full upload pipeline: batch presign → parallel binary upload → confirm each file. Returns `{ successful: FileResponse[]; failed: Array<{ filename: string; error: string }> }`.
757
+
758
+ ```typescript
759
+ // Manual: get a presigned URL, upload, confirm
760
+ const presigned = await storageApi.requestPresignedUrl({
761
+ filename: 'report.pdf',
762
+ mimeType: 'application/pdf',
763
+ size: 512000,
764
+ conversationId: 'conv-abc',
765
+ });
766
+ await platformUploadFn(presigned, file, (pct) => console.log(`${pct * 100}%`));
767
+ const fileRecord = await storageApi.confirmUpload(presigned.fileId);
768
+
769
+ // High-level: let uploadBatch handle everything
770
+ const result = await uploadBatch(
771
+ files,
772
+ platformUploadFn,
773
+ 'conv-abc',
774
+ (pct) => setProgress(pct),
775
+ );
776
+ result.successful.forEach((f) => console.log('Uploaded:', f.url));
777
+ result.failed.forEach((f) => console.error('Failed:', f.filename, f.error));
778
+ ```
779
+
780
+ ---
781
+
782
+ ### Devices API (`devicesApi`)
783
+
784
+ Used for push notification token registration. The SDK does not call this automatically — the host app is responsible for obtaining the device token from the OS and registering it.
785
+
786
+ ```typescript
787
+ import { devicesApi } from '@antzsoft/chat-core';
788
+ ```
789
+
790
+ | Method | Signature | Description |
791
+ |---|---|---|
792
+ | `register` | `(payload: RegisterDeviceTokenPayload) => Promise<void>` | Register or refresh a push token. Upserts by `deviceId`. |
793
+ | `remove` | `(deviceId: string) => Promise<void>` | Remove a device token. Call this on logout to stop push delivery. |
794
+
795
+ ```typescript
796
+ type RegisterDeviceTokenPayload =
797
+ | {
798
+ deviceId: string; // Stable UUID — generate once and persist
799
+ platform: 'ios' | 'android' | 'web';
800
+ provider: 'expo' | 'fcm' | 'apns';
801
+ token: string; // The OS-issued push token
802
+ userAgent?: string;
803
+ }
804
+ | {
805
+ deviceId: string;
806
+ platform: 'web';
807
+ provider: 'web-push';
808
+ endpoint: string; // PushSubscription.endpoint
809
+ p256dh: string; // base64url key 'p256dh'
810
+ auth: string; // base64url key 'auth'
811
+ userAgent?: string;
812
+ };
813
+ ```
814
+
815
+ ```typescript
816
+ // Mobile (Expo)
817
+ import * as Notifications from 'expo-notifications';
818
+
819
+ const { data: expoPushToken } = await Notifications.getExpoPushTokenAsync();
820
+ await devicesApi.register({
821
+ deviceId: await getStableDeviceId(), // from SecureStore
822
+ platform: 'ios',
823
+ provider: 'expo',
824
+ token: expoPushToken,
825
+ });
826
+
827
+ // On logout
828
+ await devicesApi.remove(deviceId);
829
+ ```
830
+
831
+ ---
832
+
833
+ ### Socket
834
+
835
+ #### Connection management
836
+
837
+ ```typescript
838
+ import {
839
+ connectSocket,
840
+ disconnectSocket,
841
+ reconnectSocket,
842
+ getSocketStatus,
843
+ onSocketStatus,
844
+ } from '@antzsoft/chat-core';
845
+ import type { SocketStatus } from '@antzsoft/chat-core';
846
+ ```
847
+
848
+ | Function | Signature | Description |
849
+ |---|---|---|
850
+ | `connectSocket` | `(config: ResolvedConfig, getToken: () => string \| null \| undefined) => Promise<Socket>` | Initialize and connect the Socket.IO client. No-ops if already connected. |
851
+ | `disconnectSocket` | `() => void` | Disconnect and clear the socket singleton. |
852
+ | `reconnectSocket` | `(token: string) => void` | Update the auth token on the existing socket and reconnect. |
853
+ | `getSocketStatus` | `() => SocketStatus` | Synchronously read the current connection status. |
854
+ | `onSocketStatus` | `(listener: (status: SocketStatus) => void) => () => void` | Subscribe to status changes. Returns an unsubscribe function. |
855
+
856
+ ```typescript
857
+ type SocketStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
858
+ ```
859
+
860
+ ```typescript
861
+ const unsubscribe = onSocketStatus((status) => {
862
+ if (status === 'connected') console.log('Socket ready');
863
+ if (status === 'error') console.error('Socket error — check network');
864
+ });
865
+
866
+ // Later, if the host app refreshes its own token:
867
+ reconnectSocket(newAccessToken);
868
+
869
+ // Cleanup
870
+ unsubscribe();
871
+ disconnectSocket();
872
+ ```
873
+
874
+ #### `socketEmit` — outbound events
875
+
876
+ ```typescript
877
+ import { socketEmit } from '@antzsoft/chat-core';
878
+ ```
879
+
880
+ All emit methods that have server responses use a 5-second ack timeout and return a rejected promise on timeout or when the socket is not connected. Fire-and-forget methods silently no-op when disconnected.
881
+
882
+ | Method | Signature | Behavior |
883
+ |---|---|---|
884
+ | `joinRoom` | `(conversationId: string) => void` | Join a conversation room to receive its events. Fire-and-forget. |
885
+ | `leaveRoom` | `(conversationId: string) => void` | Leave a conversation room. Fire-and-forget. |
886
+ | `sendMessage` | `(payload: SendMessagePayload) => Promise<unknown>` | Send a message. Ack-based. |
887
+ | `updateMessage` | `(messageId: string, text: string) => Promise<unknown>` | Edit a message. Ack-based. |
888
+ | `deleteMessage` | `(messageId: string) => Promise<unknown>` | Delete a message. Ack-based. |
889
+ | `addReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Add a reaction. Ack-based. |
890
+ | `removeReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Remove a reaction. Ack-based. |
891
+ | `pinMessage` | `(messageId: string) => Promise<unknown>` | Pin a message. Ack-based. |
892
+ | `unpinMessage` | `(messageId: string) => Promise<unknown>` | Unpin a message. Ack-based. |
893
+ | `typing` | `(conversationId: string, isTyping: boolean) => void` | Broadcast typing status. Fire-and-forget. |
894
+ | `markRead` | `(conversationId: string, messageId?: string) => void` | Mark messages read. Fire-and-forget. |
895
+ | `getOnlineUsers` | `(userIds: string[]) => Promise<string[]>` | Query which of the given user IDs are online. Returns the online subset. |
896
+ | `getTypingUsers` | `(conversationId: string) => Promise<unknown>` | Fetch users currently typing in a conversation. Ack-based. |
897
+
898
+ ```typescript
899
+ // Join before sending
900
+ socketEmit.joinRoom('conv-abc');
901
+
902
+ // Send a message
903
+ await socketEmit.sendMessage({
904
+ conversationId: 'conv-abc',
905
+ text: 'Hello!',
906
+ tempId: crypto.randomUUID(),
907
+ });
908
+
909
+ // Typing indicator
910
+ socketEmit.typing('conv-abc', true);
911
+ // ... user stops typing
912
+ socketEmit.typing('conv-abc', false);
913
+
914
+ // Mark messages read
915
+ socketEmit.markRead('conv-abc'); // mark all unread
916
+ socketEmit.markRead('conv-abc', 'msg-99'); // mark up to a specific message
917
+
918
+ // Check presence
919
+ const onlineIds = await socketEmit.getOnlineUsers(['user-a', 'user-b', 'user-c']);
920
+ console.log('Online:', onlineIds); // ['user-a', 'user-c']
921
+ ```
922
+
923
+ #### Socket events to listen on
924
+
925
+ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the Socket.IO socket via `getSocket().on(event, handler)`.
926
+
927
+ | Event | Payload type | Description |
928
+ |---|---|---|
929
+ | `new_message` | `NewMessageEvent` | A new message was sent to a room you've joined. |
930
+ | `message_updated` | `MessageUpdatedEvent` | A message was edited. |
931
+ | `message_deleted` | `MessageDeletedEvent` | A message was deleted. |
932
+ | `reaction_updated` | `ReactionUpdatedEvent` | Reactions on a message changed (full reaction array). |
933
+ | `typing_indicator` | `TypingIndicatorEvent` | A user started or stopped typing. |
934
+ | `user_status` | `UserStatusEvent` | A user's online/offline/away status changed. |
935
+ | `read_receipt` | `ReadReceiptEvent` | A user read messages in a conversation. |
936
+ | `message_ack` | `MessageAckEvent` | Server confirmation for a message you sent via socket (maps tempId to the real messageId). |
937
+ | `messages_delivered` | `MessagesDeliveredEvent` | Messages you sent were delivered to a recipient. |
938
+ | `conversation_created` | `Conversation` | A new conversation was created (or you were added to one). |
939
+ | `conversation_updated` | `Conversation` | A conversation's metadata was changed. |
940
+ | `conversation_deleted` | `{ conversationId: string }` | A conversation was deleted. |
941
+
942
+ ```typescript
943
+ import type {
944
+ NewMessageEvent,
945
+ MessageUpdatedEvent,
946
+ MessageDeletedEvent,
947
+ ReactionUpdatedEvent,
948
+ TypingIndicatorEvent,
949
+ UserStatusEvent,
950
+ ReadReceiptEvent,
951
+ MessageAckEvent,
952
+ MessagesDeliveredEvent,
953
+ } from '@antzsoft/chat-core';
954
+
955
+ client.socket.on('new_message', (evt: NewMessageEvent) => {
956
+ appendMessage(evt.message);
957
+ });
958
+
959
+ client.socket.on('message_updated', (evt: MessageUpdatedEvent) => {
960
+ updateMessageText(evt.messageId, evt.text);
961
+ });
962
+
963
+ client.socket.on('message_deleted', (evt: MessageDeletedEvent) => {
964
+ removeMessage(evt.messageId);
965
+ });
966
+
967
+ client.socket.on('reaction_updated', (evt: ReactionUpdatedEvent) => {
968
+ setMessageReactions(evt.messageId, evt.reactions);
969
+ });
970
+
971
+ client.socket.on('typing_indicator', (evt: TypingIndicatorEvent) => {
972
+ if (evt.isTyping) {
973
+ showTyping(evt.conversationId, evt.displayName);
974
+ } else {
975
+ hideTyping(evt.conversationId, evt.userId);
976
+ }
977
+ });
978
+
979
+ client.socket.on('read_receipt', (evt: ReadReceiptEvent) => {
980
+ markMessagesRead(evt.fullyReadMessageIds ?? []);
981
+ });
982
+
983
+ client.socket.on('message_ack', (evt: MessageAckEvent) => {
984
+ // Replace optimistic message
985
+ confirmOptimisticMessage(evt.tempId, evt.messageId, evt.status);
986
+ });
987
+ ```
988
+
989
+ ---
990
+
991
+ ### Auth Store
992
+
993
+ The auth store is a Zustand store that persists user and token state through the configured `PersistStorage` adapter under the key `"antz-chat-auth"`.
994
+
995
+ ```typescript
996
+ import { initAuthStore, getAuthStore, resetAuthStore } from '@antzsoft/chat-core';
997
+ ```
998
+
999
+ | Function | Description |
1000
+ |---|---|
1001
+ | `initAuthStore(storage: PersistStorage)` | Initialize the auth store singleton. Idempotent — safe to call multiple times. Returns `{ useAuthStore, authTokenStore }`. |
1002
+ | `getAuthStore()` | Retrieve the singleton after initialization. Throws if called before `initAuthStore`. |
1003
+ | `resetAuthStore()` | Clear the singleton (for testing or SDK teardown). |
1004
+
1005
+ #### `useAuthStore` — Zustand store
1006
+
1007
+ ```typescript
1008
+ interface AuthState {
1009
+ user: User | null;
1010
+ tokens: AuthTokens | null;
1011
+ isAuthenticated: boolean;
1012
+ isLoading: boolean;
1013
+ /** True once persisted state has been rehydrated from storage. */
1014
+ isHydrated: boolean;
1015
+
1016
+ setAuth(user: User, tokens: AuthTokens): void;
1017
+ setTokens(tokens: AuthTokens): void;
1018
+ setUser(user: User): void;
1019
+ logout(): void;
1020
+ setLoading(loading: boolean): void;
1021
+ setHydrated(hydrated: boolean): void;
1022
+ }
1023
+ ```
1024
+
1025
+ ```typescript
1026
+ // In a React component (web or RN)
1027
+ const { useAuthStore } = getAuthStore();
1028
+ const user = useAuthStore((s) => s.user);
1029
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
1030
+
1031
+ // Outside React — read state directly
1032
+ const { useAuthStore } = getAuthStore();
1033
+ const currentUser = useAuthStore.getState().user;
1034
+ ```
1035
+
1036
+ #### `authTokenStore` — raw token operations
1037
+
1038
+ Thin synchronous wrapper used internally by the API client and socket for token access.
1039
+
1040
+ ```typescript
1041
+ const { authTokenStore } = getAuthStore();
1042
+
1043
+ authTokenStore.getAccessToken(); // string | null | undefined
1044
+ authTokenStore.getRefreshToken(); // string | null | undefined
1045
+ authTokenStore.setTokens(tokens); // update stored tokens
1046
+ authTokenStore.clearTokens(); // clear tokens and log out
1047
+ ```
1048
+
1049
+ ---
1050
+
1051
+ ### Chat Store (`useChatStore`)
1052
+
1053
+ A non-persisted Zustand store for transient UI state. Import and use in any component or hook.
1054
+
1055
+ ```typescript
1056
+ import { useChatStore } from '@antzsoft/chat-core';
1057
+ ```
1058
+
1059
+ #### State
1060
+
1061
+ | Field | Type | Description |
1062
+ |---|---|---|
1063
+ | `activeConversationId` | `string \| null` | The currently open conversation. |
1064
+ | `pendingTarget` | `{ conversationId: string; messageId: string } \| null` | Scroll-to target for deep-linked messages. |
1065
+ | `typingUsers` | `Record<string, TypingUser[]>` | Map of conversationId → users currently typing. |
1066
+ | `onlineUsers` | `string[]` | Array of user IDs currently online. |
1067
+ | `replyingTo` | `Message \| null` | Message being replied to in the composer. |
1068
+ | `editingMessage` | `Message \| null` | Message being edited in the composer. |
1069
+ | `isSidebarOpen` | `boolean` | Conversation list sidebar visibility. |
1070
+ | `isGroupInfoOpen` | `boolean` | Group info panel visibility. |
1071
+
1072
+ #### Actions
1073
+
1074
+ | Action | Signature | Description |
1075
+ |---|---|---|
1076
+ | `setActiveConversation` | `(id: string \| null) => void` | Set active conversation; clears `replyingTo` and `editingMessage`. |
1077
+ | `setPendingTarget` | `(target: { conversationId: string; messageId: string } \| null) => void` | Set scroll target for deep links. |
1078
+ | `addTypingUser` | `(conversationId: string, user: TypingUser) => void` | Add or refresh a typing user. De-duplicates by userId. |
1079
+ | `removeTypingUser` | `(conversationId: string, userId: string) => void` | Remove a user from the typing list. |
1080
+ | `setUserOnline` | `(userId: string) => void` | Mark a user as online. |
1081
+ | `setUserOffline` | `(userId: string) => void` | Mark a user as offline. |
1082
+ | `setOnlineUsers` | `(userIds: string[]) => void` | Replace the full online users list. |
1083
+ | `setReplyingTo` | `(message: Message \| null) => void` | Set reply context; clears `editingMessage`. |
1084
+ | `setEditingMessage` | `(message: Message \| null) => void` | Set edit context; clears `replyingTo`. |
1085
+ | `toggleSidebar` | `() => void` | Toggle sidebar open/closed. |
1086
+ | `setSidebarOpen` | `(open: boolean) => void` | Set sidebar state explicitly. |
1087
+ | `toggleGroupInfo` | `() => void` | Toggle group info panel. |
1088
+ | `setGroupInfoOpen` | `(open: boolean) => void` | Set group info panel state explicitly. |
1089
+
1090
+ ```typescript
1091
+ // In a component
1092
+ const activeId = useChatStore((s) => s.activeConversationId);
1093
+ const typingInActive = useChatStore((s) =>
1094
+ activeId ? (s.typingUsers[activeId] ?? []) : [],
1095
+ );
1096
+ const { setActiveConversation, setReplyingTo } = useChatStore.getState();
1097
+
1098
+ // Wire typing indicator events
1099
+ client.socket.on('typing_indicator', (evt: TypingIndicatorEvent) => {
1100
+ const { addTypingUser, removeTypingUser } = useChatStore.getState();
1101
+ if (evt.isTyping) {
1102
+ addTypingUser(evt.conversationId, {
1103
+ userId: evt.userId,
1104
+ displayName: evt.displayName,
1105
+ avatarUrl: evt.avatarUrl,
1106
+ });
1107
+ } else {
1108
+ removeTypingUser(evt.conversationId, evt.userId);
1109
+ }
1110
+ });
1111
+
1112
+ // Wire user status events
1113
+ client.socket.on('user_status', (evt: UserStatusEvent) => {
1114
+ const { setUserOnline, setUserOffline } = useChatStore.getState();
1115
+ if (evt.status === 'online') {
1116
+ setUserOnline(evt.userId);
1117
+ } else {
1118
+ setUserOffline(evt.userId);
1119
+ }
1120
+ });
1121
+ ```
1122
+
1123
+ ---
1124
+
1125
+ ## Data Types
1126
+
1127
+ ### `User`
1128
+
1129
+ ```typescript
1130
+ interface User {
1131
+ id: string;
1132
+ tenantId: string;
1133
+ email: string;
1134
+ username: string;
1135
+ displayName: string;
1136
+ avatarUrl?: string;
1137
+ phone?: string;
1138
+ status: 'online' | 'offline' | 'away';
1139
+ lastSeenAt?: string; // ISO 8601
1140
+ createdAt: string;
1141
+ updatedAt: string;
1142
+ }
1143
+ ```
1144
+
1145
+ ### `AuthTokens`
1146
+
1147
+ ```typescript
1148
+ interface AuthTokens {
1149
+ accessToken: string;
1150
+ refreshToken: string;
1151
+ tokenType: string; // 'Bearer'
1152
+ expiresIn: number; // seconds
1153
+ }
1154
+ ```
1155
+
1156
+ ### `Message`
1157
+
1158
+ ```typescript
1159
+ interface Message {
1160
+ id: string;
1161
+ tenantId: string;
1162
+ conversationId: string;
1163
+ senderId: string;
1164
+ content: MessageContent;
1165
+ replyTo?: MessageReplyReference;
1166
+ reactions: MessageReaction[];
1167
+ status: 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';
1168
+ deliveryStatus?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
1169
+ isEdited: boolean;
1170
+ editedAt?: string;
1171
+ isStarred?: boolean;
1172
+ isPinned?: boolean;
1173
+ pinnedBy?: string;
1174
+ pinnedAt?: string;
1175
+ uploadProgress?: number; // 0–100, present on optimistic messages
1176
+ sentAt: string;
1177
+ createdAt: string;
1178
+ sender?: User;
1179
+ readBy?: string[];
1180
+ isEncrypted?: boolean;
1181
+ encryptionMode?: 'none' | 'server' | 'e2ee';
1182
+ encryptedContent?: EncryptedContent;
1183
+ }
1184
+ ```
1185
+
1186
+ ### `MessageContent`
1187
+
1188
+ ```typescript
1189
+ interface MessageContent {
1190
+ type: 'text' | 'attachment' | 'system';
1191
+ text?: string;
1192
+ attachments?: Attachment[];
1193
+ }
1194
+ ```
1195
+
1196
+ ### `MessageReaction`
1197
+
1198
+ ```typescript
1199
+ interface MessageReaction {
1200
+ emoji: string;
1201
+ userIds: string[];
1202
+ count: number;
1203
+ }
1204
+ ```
1205
+
1206
+ ### `MessageReplyReference`
1207
+
1208
+ ```typescript
1209
+ interface MessageReplyReference {
1210
+ messageId?: string;
1211
+ contentPreview?: string;
1212
+ senderName?: string;
1213
+ // Present on optimistic messages before server confirmation
1214
+ id?: string;
1215
+ content?: MessageContent;
1216
+ sender?: Pick<User, 'displayName' | 'avatarUrl'>;
1217
+ }
1218
+ ```
1219
+
1220
+ ### `Conversation`
1221
+
1222
+ ```typescript
1223
+ interface Conversation {
1224
+ id: string;
1225
+ tenantId: string;
1226
+ conversationType: 'direct' | 'group';
1227
+ name?: string;
1228
+ description?: string;
1229
+ icon?: string;
1230
+ iconUrl?: string;
1231
+ participants: Participant[];
1232
+ participantCount?: number;
1233
+ settings?: ConversationSettings;
1234
+ lastMessage?: Message;
1235
+ createdBy?: string;
1236
+ isActive: boolean;
1237
+ createdAt: string;
1238
+ updatedAt: string;
1239
+ unreadCount?: number;
1240
+ isPinned?: boolean;
1241
+ isMuted?: boolean;
1242
+ mutedUntil?: string;
1243
+ encryptionMode?: 'none' | 'server' | 'e2ee';
1244
+ isEncryptionEnabled?: boolean;
1245
+ encryptionKey?: string;
1246
+ }
1247
+
1248
+ interface ConversationSettings {
1249
+ onlyAdminsCanMessage?: boolean;
1250
+ onlyAdminsCanAddMembers?: boolean;
1251
+ }
1252
+ ```
1253
+
1254
+ ### `Participant`
1255
+
1256
+ ```typescript
1257
+ interface Participant {
1258
+ userId: string;
1259
+ role: 'admin' | 'member';
1260
+ joinedAt: string;
1261
+ isActive?: boolean;
1262
+ user?: User;
1263
+ }
1264
+ ```
1265
+
1266
+ ### `Attachment`
1267
+
1268
+ ```typescript
1269
+ interface Attachment {
1270
+ id: string;
1271
+ type: 'image' | 'video' | 'audio' | 'document';
1272
+ url: string;
1273
+ thumbnailUrl?: string;
1274
+ filename: string;
1275
+ mimeType: string;
1276
+ size: number; // bytes
1277
+ dimensions?: { width: number; height: number };
1278
+ duration?: number; // seconds, for audio/video
1279
+ isUploading?: boolean;
1280
+ uploadProgress?: number; // 0–100
1281
+ }
1282
+ ```
1283
+
1284
+ ### `UploadableFile`
1285
+
1286
+ Platform-agnostic file descriptor. Web builds it from a `File` object; RN builds it from a document/image picker result.
1287
+
1288
+ ```typescript
1289
+ interface UploadableFile {
1290
+ uri: string; // blob URL on web, file:// URI on RN
1291
+ name: string;
1292
+ type: string; // MIME type, e.g. "image/jpeg"
1293
+ size: number; // bytes
1294
+ }
1295
+ ```
1296
+
1297
+ ### `SendMessagePayload`
1298
+
1299
+ ```typescript
1300
+ interface SendMessagePayload {
1301
+ conversationId: string;
1302
+ text?: string;
1303
+ attachments?: SendMessageAttachment[];
1304
+ replyTo?: string; // messageId
1305
+ tempId: string; // client-generated; echoed back in message_ack
1306
+ encryptedContent?: EncryptedContent;
1307
+ isEncrypted?: boolean;
1308
+ }
1309
+
1310
+ interface SendMessageAttachment {
1311
+ fileId: string;
1312
+ type: 'image' | 'video' | 'audio' | 'document';
1313
+ url: string;
1314
+ thumbnailUrl?: string;
1315
+ filename: string;
1316
+ mimeType: string;
1317
+ size: number;
1318
+ }
1319
+ ```
1320
+
1321
+ ### Socket event types
1322
+
1323
+ ```typescript
1324
+ interface NewMessageEvent {
1325
+ tempId?: string;
1326
+ senderName?: string;
1327
+ senderAvatarUrl?: string;
1328
+ message: Message;
1329
+ }
1330
+
1331
+ interface MessageUpdatedEvent {
1332
+ messageId: string;
1333
+ conversationId: string;
1334
+ text: string;
1335
+ editedAt: string;
1336
+ }
1337
+
1338
+ interface MessageDeletedEvent {
1339
+ messageId: string;
1340
+ conversationId: string;
1341
+ }
1342
+
1343
+ interface ReactionUpdatedEvent {
1344
+ messageId: string;
1345
+ conversationId: string;
1346
+ reactions: MessageReaction[];
1347
+ }
1348
+
1349
+ interface TypingIndicatorEvent {
1350
+ conversationId: string;
1351
+ userId: string;
1352
+ username: string;
1353
+ displayName: string;
1354
+ avatarUrl?: string;
1355
+ isTyping: boolean;
1356
+ }
1357
+
1358
+ interface UserStatusEvent {
1359
+ userId: string;
1360
+ status: 'online' | 'offline' | 'away';
1361
+ lastSeenAt?: string;
1362
+ }
1363
+
1364
+ interface ReadReceiptEvent {
1365
+ conversationId: string;
1366
+ messageId: string;
1367
+ userId: string;
1368
+ readAt: string;
1369
+ updatedMessageIds?: string[];
1370
+ fullyReadMessageIds?: string[];
1371
+ }
1372
+
1373
+ interface MessageAckEvent {
1374
+ tempId: string; // the client-generated ID you sent
1375
+ messageId: string; // the server-assigned real ID
1376
+ status: MessageStatus;
1377
+ }
1378
+
1379
+ interface MessagesDeliveredEvent {
1380
+ conversationId: string;
1381
+ messageIds: string[];
1382
+ deliveredTo: string;
1383
+ deliveredAt: string;
1384
+ }
1385
+ ```
1386
+
1387
+ ---
1388
+
1389
+ ## Building a Custom Integration
1390
+
1391
+ This example shows a complete headless Node.js integration — useful for bots, message archiving, or any backend consumer that needs to interact with Antz Chat.
1392
+
1393
+ ```typescript
1394
+ // node-bot.ts
1395
+ import {
1396
+ AntzChatClient,
1397
+ type AntzChatConfig,
1398
+ type NewMessageEvent,
1399
+ type UploadableFile,
1400
+ } from '@antzsoft/chat-core';
1401
+ import { readFileSync } from 'fs';
1402
+
1403
+ // ─── 1. In-memory storage adapter for Node.js ───────────────────────────────
1404
+ const _store: Record<string, string> = {};
1405
+ const nodeStorage = {
1406
+ getItem: (key: string) => _store[key] ?? null,
1407
+ setItem: (key: string, value: string) => { _store[key] = value; },
1408
+ removeItem: (key: string) => { delete _store[key]; },
1409
+ };
1410
+
1411
+ // ─── 2. Node.js upload adapter using fetch ──────────────────────────────────
1412
+ const nodePlatformUploadFn: AntzChatConfig['platformUploadFn'] = async (presigned, file) => {
1413
+ const body = readFileSync(file.uri.replace('file://', ''));
1414
+ const res = await fetch(presigned.uploadUrl, {
1415
+ method: presigned.method,
1416
+ headers: presigned.headers,
1417
+ body,
1418
+ });
1419
+ if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
1420
+ };
1421
+
1422
+ // ─── 3. Initialize the client ───────────────────────────────────────────────
1423
+ const client = new AntzChatClient({
1424
+ apiUrl: 'https://api.yourapp.com/api/v1',
1425
+ persistStorage: nodeStorage,
1426
+ platformUploadFn: nodePlatformUploadFn,
1427
+ tenantId: 'tenant-xyz',
1428
+ });
1429
+
1430
+ // ─── 4. Authenticate ────────────────────────────────────────────────────────
1431
+ await client.auth.login({ email: 'bot@yourapp.com', password: process.env.BOT_PASSWORD! });
1432
+
1433
+ // ─── 5. Connect socket ──────────────────────────────────────────────────────
1434
+ await client.connect();
1435
+
1436
+ // ─── 6. Fetch conversations and join all rooms ──────────────────────────────
1437
+ const { data: conversations } = await client.conversations.list({ limit: 100 });
1438
+ for (const conv of conversations) {
1439
+ client.socket.emit.joinRoom(conv.id);
1440
+ }
1441
+
1442
+ // ─── 7. Listen and respond to messages ──────────────────────────────────────
1443
+ client.socket.on('new_message', async (evt: NewMessageEvent) => {
1444
+ const { message } = evt;
1445
+
1446
+ // Ignore messages sent by the bot itself
1447
+ const me = await client.auth.getMe();
1448
+ if (message.senderId === me.id) return;
1449
+
1450
+ const text = message.content.text?.toLowerCase() ?? '';
1451
+
1452
+ if (text.includes('ping')) {
1453
+ await client.socket.emit.sendMessage({
1454
+ conversationId: message.conversationId,
1455
+ text: 'Pong!',
1456
+ tempId: crypto.randomUUID(),
1457
+ });
1458
+ }
1459
+
1460
+ // Mark message as read
1461
+ client.socket.emit.markRead(message.conversationId, message.id);
1462
+ });
1463
+
1464
+ // ─── 8. Graceful shutdown ───────────────────────────────────────────────────
1465
+ process.on('SIGINT', () => {
1466
+ client.disconnect();
1467
+ process.exit(0);
1468
+ });
1469
+ ```
1470
+
1471
+ **Custom UI (no framework):**
1472
+
1473
+ ```typescript
1474
+ // custom-ui.ts — vanilla browser without the web SDK
1475
+ import {
1476
+ AntzChatClient,
1477
+ useChatStore,
1478
+ type NewMessageEvent,
1479
+ } from '@antzsoft/chat-core';
1480
+
1481
+ const client = new AntzChatClient({ apiUrl, persistStorage, platformUploadFn });
1482
+
1483
+ await client.auth.login({ email, password });
1484
+ await client.connect();
1485
+
1486
+ // Sync socket events into the chat store
1487
+ client.socket.on('new_message', (evt: NewMessageEvent) => {
1488
+ renderMessage(evt.message);
1489
+ client.socket.emit.markRead(evt.message.conversationId, evt.message.id);
1490
+ });
1491
+
1492
+ // Use chatStore for local UI state
1493
+ const { setActiveConversation, setReplyingTo } = useChatStore.getState();
1494
+
1495
+ document.querySelectorAll('[data-conv-id]').forEach((el) => {
1496
+ el.addEventListener('click', () => {
1497
+ const convId = el.getAttribute('data-conv-id')!;
1498
+ setActiveConversation(convId);
1499
+ client.socket.emit.joinRoom(convId);
1500
+ });
1501
+ });
1502
+ ```
1503
+
1504
+ ---
1505
+
1506
+ ## License
1507
+
1508
+ MIT