@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 +1508 -0
- package/dist/index.cjs +811 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +852 -0
- package/dist/index.d.ts +852 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
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
|
+
[](https://www.npmjs.com/package/@antzsoft/chat-core)
|
|
6
|
+
[](./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
|