@manonero/chat-client-sdk 1.0.0-beta.0 → 1.0.0-beta.10
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 +432 -128
- package/dist/ChatClient.d.ts +67 -8
- package/dist/ChatClient.d.ts.map +1 -1
- package/dist/ChatClient.js +82 -9
- package/dist/ChatClient.js.map +1 -1
- package/dist/errors/ChatApiError.d.ts +21 -4
- package/dist/errors/ChatApiError.d.ts.map +1 -1
- package/dist/errors/ChatApiError.js +12 -6
- package/dist/errors/ChatApiError.js.map +1 -1
- package/dist/http/BotApi.d.ts +6 -13
- package/dist/http/BotApi.d.ts.map +1 -1
- package/dist/http/BotApi.js +4 -11
- package/dist/http/BotApi.js.map +1 -1
- package/dist/http/ConversationApi.d.ts +42 -8
- package/dist/http/ConversationApi.d.ts.map +1 -1
- package/dist/http/ConversationApi.js +57 -8
- package/dist/http/ConversationApi.js.map +1 -1
- package/dist/http/FileApi.d.ts +16 -4
- package/dist/http/FileApi.d.ts.map +1 -1
- package/dist/http/FileApi.js +29 -6
- package/dist/http/FileApi.js.map +1 -1
- package/dist/http/HttpClient.d.ts +20 -2
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +76 -14
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/MessageApi.d.ts +6 -4
- package/dist/http/MessageApi.d.ts.map +1 -1
- package/dist/http/MessageApi.js +17 -4
- package/dist/http/MessageApi.js.map +1 -1
- package/dist/http/ProxyApi.d.ts +18 -2
- package/dist/http/ProxyApi.d.ts.map +1 -1
- package/dist/http/ProxyApi.js +33 -6
- package/dist/http/ProxyApi.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/realtime/ChatHubClient.d.ts +58 -1
- package/dist/realtime/ChatHubClient.d.ts.map +1 -1
- package/dist/realtime/ChatHubClient.js +202 -15
- package/dist/realtime/ChatHubClient.js.map +1 -1
- package/dist/realtime/NotificationHubClient.d.ts +43 -1
- package/dist/realtime/NotificationHubClient.d.ts.map +1 -1
- package/dist/realtime/NotificationHubClient.js +134 -29
- package/dist/realtime/NotificationHubClient.js.map +1 -1
- package/dist/realtime/ReconnectionManager.d.ts +2 -1
- package/dist/realtime/ReconnectionManager.d.ts.map +1 -1
- package/dist/realtime/ReconnectionManager.js +13 -3
- package/dist/realtime/ReconnectionManager.js.map +1 -1
- package/dist/types/auth.d.ts +4 -2
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/block.d.ts +16 -1
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/block.js +30 -0
- package/dist/types/block.js.map +1 -1
- package/dist/types/bot.d.ts +2 -8
- package/dist/types/bot.d.ts.map +1 -1
- package/dist/types/chat-events.d.ts +25 -3
- package/dist/types/chat-events.d.ts.map +1 -1
- package/dist/types/common.d.ts +26 -3
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/conversation.d.ts +65 -15
- package/dist/types/conversation.d.ts.map +1 -1
- package/dist/types/message.d.ts +9 -5
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/notification-events.d.ts +36 -9
- package/dist/types/notification-events.d.ts.map +1 -1
- package/dist/types/participant.d.ts +4 -4
- package/dist/types/participant.d.ts.map +1 -1
- package/dist/types/signalr.d.ts +6 -3
- package/dist/types/signalr.d.ts.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -61,39 +61,39 @@ ChatClient (facade chính)
|
|
|
61
61
|
### Installation
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
npm install chat-client-sdk
|
|
64
|
+
npm install @manonero/chat-client-sdk
|
|
65
65
|
npm install @microsoft/signalr # peer dependency
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
### Import
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
|
-
import { ChatClient } from 'chat-client-sdk';
|
|
71
|
+
import { ChatClient } from '@manonero/chat-client-sdk';
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Tất cả các kiểu dữ liệu đều được export từ package root:
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
|
-
import type { MessageDto, ConversationDto, Block } from 'chat-client-sdk';
|
|
78
|
-
import { ChatApiError } from 'chat-client-sdk'; // class — dùng import (không phải import type) để hỗ trợ instanceof
|
|
77
|
+
import type { MessageDto, ConversationDto, Block } from '@manonero/chat-client-sdk';
|
|
78
|
+
import { ChatApiError } from '@manonero/chat-client-sdk'; // class — dùng import (không phải import type) để hỗ trợ instanceof
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**Export nâng cao** — SDK cũng export một số class và type nội bộ cho power users:
|
|
82
82
|
|
|
83
83
|
```ts
|
|
84
84
|
// TypedEventEmitter — dùng độc lập hoặc extend
|
|
85
|
-
import { TypedEventEmitter } from 'chat-client-sdk';
|
|
86
|
-
import type { Unsubscribe } from 'chat-client-sdk';
|
|
85
|
+
import { TypedEventEmitter } from '@manonero/chat-client-sdk';
|
|
86
|
+
import type { Unsubscribe } from '@manonero/chat-client-sdk';
|
|
87
87
|
|
|
88
88
|
// HttpClient — tự xây dựng API module bổ sung theo cùng pattern
|
|
89
|
-
import { HttpClient } from 'chat-client-sdk';
|
|
90
|
-
import type { HttpClientOptions } from 'chat-client-sdk';
|
|
89
|
+
import { HttpClient } from '@manonero/chat-client-sdk';
|
|
90
|
+
import type { HttpClientOptions } from '@manonero/chat-client-sdk';
|
|
91
91
|
|
|
92
92
|
// Event map types — dùng khi cần typed wrapper
|
|
93
|
-
import type { ChatHubEventMap, NotificationHubEventMap } from 'chat-client-sdk';
|
|
93
|
+
import type { ChatHubEventMap, NotificationHubEventMap } from '@manonero/chat-client-sdk';
|
|
94
94
|
|
|
95
95
|
// Hub options (khi dùng hub client độc lập, không qua ChatClient)
|
|
96
|
-
import type { ChatHubClientOptions, NotificationHubClientOptions } from 'chat-client-sdk';
|
|
96
|
+
import type { ChatHubClientOptions, NotificationHubClientOptions } from '@manonero/chat-client-sdk';
|
|
97
97
|
|
|
98
98
|
// Hub request types (Client → Server) — dùng khi gọi SignalR methods
|
|
99
99
|
import type {
|
|
@@ -103,7 +103,7 @@ import type {
|
|
|
103
103
|
ChatRecoverMessageRequest,
|
|
104
104
|
ChatAddReactionRequest,
|
|
105
105
|
ChatRemoveReactionRequest,
|
|
106
|
-
} from 'chat-client-sdk';
|
|
106
|
+
} from '@manonero/chat-client-sdk';
|
|
107
107
|
|
|
108
108
|
// Ack types (Server → Client return) — dùng khi cần type return value của Hub methods
|
|
109
109
|
import type {
|
|
@@ -114,22 +114,22 @@ import type {
|
|
|
114
114
|
ReactionAck,
|
|
115
115
|
MarkAsReadAck,
|
|
116
116
|
HubErrorDto,
|
|
117
|
-
} from 'chat-client-sdk';
|
|
117
|
+
} from '@manonero/chat-client-sdk';
|
|
118
118
|
|
|
119
119
|
// ReconnectionManager options
|
|
120
|
-
import type { ReconnectionManagerOptions } from 'chat-client-sdk';
|
|
120
|
+
import type { ReconnectionManagerOptions } from '@manonero/chat-client-sdk';
|
|
121
121
|
|
|
122
122
|
// ChatClient facade types
|
|
123
|
-
import type { ChatClientSignalrOptions, ChatClientRealtime } from 'chat-client-sdk';
|
|
123
|
+
import type { ChatClientSignalrOptions, ChatClientRealtime } from '@manonero/chat-client-sdk';
|
|
124
124
|
|
|
125
125
|
// Upload progress callback type — dùng khi cần type hàm onProgress
|
|
126
|
-
import type { UploadProgressCallback } from 'chat-client-sdk';
|
|
126
|
+
import type { UploadProgressCallback } from '@manonero/chat-client-sdk';
|
|
127
127
|
|
|
128
128
|
// RFC 7807 Problem Details — dùng khi cần parse/tạo lỗi thủ công
|
|
129
|
-
import type { ProblemDetails } from 'chat-client-sdk';
|
|
129
|
+
import type { ProblemDetails, ProblemError } from '@manonero/chat-client-sdk';
|
|
130
130
|
|
|
131
131
|
// Mark as read request type (REST)
|
|
132
|
-
import type { MarkAsReadRequest } from 'chat-client-sdk';
|
|
132
|
+
import type { MarkAsReadRequest } from '@manonero/chat-client-sdk';
|
|
133
133
|
|
|
134
134
|
// Login request types riêng lẻ (khi cần type-check từng provider)
|
|
135
135
|
import type {
|
|
@@ -137,10 +137,10 @@ import type {
|
|
|
137
137
|
DsAccountLoginRequest,
|
|
138
138
|
DevLoginRequest,
|
|
139
139
|
LoginRequest,
|
|
140
|
-
} from 'chat-client-sdk';
|
|
140
|
+
} from '@manonero/chat-client-sdk';
|
|
141
141
|
|
|
142
142
|
// Standalone hub clients — dùng khi không cần ChatClient facade
|
|
143
|
-
import { ChatHubClient, NotificationHubClient, ReconnectionManager } from 'chat-client-sdk';
|
|
143
|
+
import { ChatHubClient, NotificationHubClient, ReconnectionManager } from '@manonero/chat-client-sdk';
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
### Khởi tạo cơ bản
|
|
@@ -171,7 +171,12 @@ const client = new ChatClient({
|
|
|
171
171
|
});
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
> **Ưu tiên token
|
|
174
|
+
> **Ưu tiên token (resolve mỗi request / connect):**
|
|
175
|
+
> 1. Nếu `tokenProvider` được cấu hình **VÀ** trả về string non-empty → dùng giá trị đó.
|
|
176
|
+
> 2. Ngược lại → dùng token nội bộ (`token` constructor / `setToken()` / auto-set sau login).
|
|
177
|
+
> 3. Ngược lại → `null` (không gửi `Authorization`).
|
|
178
|
+
>
|
|
179
|
+
> Provider luôn được hỏi trước nên các hệ thống refresh-token rotation tiếp tục hoạt động kể cả sau khi `setToken()` đã được gọi. Dùng `clearToken()` nếu muốn xoá token nội bộ và trả quyền hoàn toàn về provider.
|
|
175
180
|
|
|
176
181
|
### Cấu hình SignalR log level
|
|
177
182
|
|
|
@@ -205,6 +210,7 @@ new ChatClient(options: ChatClientOptions)
|
|
|
205
210
|
| `baseUrl` | `string` | ✅ | URL gốc của server, ví dụ `"https://chat-api.example.com"` |
|
|
206
211
|
| `token` | `string` | ❌ | JWT token ban đầu (nếu đã đăng nhập) |
|
|
207
212
|
| `tokenProvider` | `() => string \| null` | ❌ | Hàm lấy token từ bên ngoài |
|
|
213
|
+
| `requestTimeoutMs` | `number \| null` | ❌ | Timeout HTTP / upload tính bằng ms. Mặc định `30000`. Truyền `null` để tắt (hữu ích khi upload file lớn). |
|
|
208
214
|
| `signalrOptions.logLevel` | `LogLevel` | ❌ | Mức log cho SignalR (mặc định: `Warning`) |
|
|
209
215
|
|
|
210
216
|
### Thuộc tính
|
|
@@ -225,20 +231,52 @@ new ChatClient(options: ChatClientOptions)
|
|
|
225
231
|
|
|
226
232
|
### Phương thức
|
|
227
233
|
|
|
228
|
-
#### `setToken(token: string): void`
|
|
234
|
+
#### `setToken(token: string | null): void`
|
|
229
235
|
|
|
230
|
-
Cập nhật JWT token
|
|
236
|
+
Cập nhật JWT token nội bộ. Có hiệu lực **ngay** với mọi HTTP request tiếp theo. Truyền `null` để xoá token nội bộ và `currentUser` (tương đương `clearToken()`).
|
|
231
237
|
|
|
232
|
-
> ⚠️ Các kết nối SignalR
|
|
238
|
+
> ⚠️ Các kết nối SignalR **đang hoạt động** vẫn dùng token cũ cho đến lúc reconnect. Gọi `refreshConnections()` để áp token mới ngay cho hub đang Connected.
|
|
239
|
+
|
|
240
|
+
> ℹ️ Nếu `tokenProvider` đã được cấu hình và vẫn trả về giá trị, provider tiếp tục được ưu tiên — `setToken()` chỉ ảnh hưởng khi provider trả `null`/empty.
|
|
233
241
|
|
|
234
242
|
```ts
|
|
235
243
|
client.setToken('new-jwt-token-here');
|
|
244
|
+
await client.refreshConnections(); // tuỳ chọn — chỉ khi muốn áp ngay cho SignalR
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `clearToken(): void`
|
|
248
|
+
|
|
249
|
+
Xoá token nội bộ và `currentUser`. Không động tới `tokenProvider` (provider tiếp tục hoạt động bình thường) và không ngắt SignalR. Dùng `logout()` nếu muốn ngắt cả hub.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
client.clearToken();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `refreshConnections(): Promise<void>`
|
|
256
|
+
|
|
257
|
+
`disconnect()` + `connect()` lại từng hub đang ở trạng thái `Connected`. Hub đang Disconnected/Reconnecting được giữ nguyên. Dùng để áp token mới (sau `setToken()`) cho phiên SignalR đang chạy.
|
|
258
|
+
|
|
259
|
+
> ⚠️ `disconnect()` xoá local tracking (`joinedConversations`, `subscribedPresenceIds`). Sau khi gọi `refreshConnections()`, ứng dụng phải tự `joinConversation` / `subscribeToPresence` lại — hoặc dùng `ReconnectionManager` để khôi phục trong suốt cho mọi loại disconnect.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
client.setToken(newToken);
|
|
263
|
+
await client.refreshConnections();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `logout(): Promise<void>`
|
|
267
|
+
|
|
268
|
+
Xoá token + `currentUser` rồi `disconnect()` cả hai hub. Chỉ clear local state — không gọi server (server không có endpoint logout). Nếu `tokenProvider` được cấu hình và vẫn trả token, request kế tiếp vẫn có thể auth lại.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await client.logout();
|
|
236
272
|
```
|
|
237
273
|
|
|
238
274
|
#### `disconnect(): Promise<void>`
|
|
239
275
|
|
|
240
276
|
Ngắt kết nối tất cả các SignalR hub (ChatHub + NotificationHub) đồng thời.
|
|
241
277
|
|
|
278
|
+
> Cả hai hub set cờ `intentionallyClosed` trước khi tear down nên `ReconnectionManager` (nếu được attach) sẽ **bỏ qua** lần `disconnected` này thay vì auto-reconnect ngược ý người dùng.
|
|
279
|
+
|
|
242
280
|
```ts
|
|
243
281
|
await client.disconnect();
|
|
244
282
|
```
|
|
@@ -297,7 +335,8 @@ interface AuthUserInfo {
|
|
|
297
335
|
id: string;
|
|
298
336
|
uniqueName: string;
|
|
299
337
|
fullName: string;
|
|
300
|
-
avatar?: string;
|
|
338
|
+
avatar?: string | null; // URL thuần (KHÔNG phải MediaReference); server có thể trả null
|
|
339
|
+
role?: string | null; // 'dev' với dev users; null với Google/DsAccount users
|
|
301
340
|
}
|
|
302
341
|
```
|
|
303
342
|
|
|
@@ -350,13 +389,13 @@ interface ParticipantDto {
|
|
|
350
389
|
id: string;
|
|
351
390
|
uniqueName: string;
|
|
352
391
|
fullName: string;
|
|
353
|
-
avatar?: MediaReference;
|
|
354
|
-
gender?: string;
|
|
392
|
+
avatar?: MediaReference | null; // hỗ trợ cả internal storageKey lẫn external URL; server có thể trả null
|
|
393
|
+
gender?: string | null;
|
|
355
394
|
isBot: boolean;
|
|
356
395
|
isOnline: boolean;
|
|
357
|
-
lastSeenAt?: string; // ISO 8601
|
|
358
|
-
createdAt: string;
|
|
359
|
-
updatedAt: string;
|
|
396
|
+
lastSeenAt?: string | null; // ISO 8601, null khi đang online
|
|
397
|
+
createdAt: string; // ISO 8601
|
|
398
|
+
updatedAt: string; // ISO 8601
|
|
360
399
|
}
|
|
361
400
|
```
|
|
362
401
|
|
|
@@ -368,7 +407,7 @@ interface ParticipantDto {
|
|
|
368
407
|
|
|
369
408
|
Tất cả endpoint đều **yêu cầu JWT**.
|
|
370
409
|
|
|
371
|
-
### `list(params?): Promise<
|
|
410
|
+
### `list(params?): Promise<CursorPaginatedResult<ConversationListItemDto>>`
|
|
372
411
|
|
|
373
412
|
Lấy danh sách cuộc hội thoại với cursor-based pagination. Sắp xếp: pinned trước, sau đó theo `lastMessageAt` giảm dần.
|
|
374
413
|
|
|
@@ -388,7 +427,7 @@ if (result.hasMore) {
|
|
|
388
427
|
}
|
|
389
428
|
```
|
|
390
429
|
|
|
391
|
-
> ⚠️ `cursor` là chuỗi
|
|
430
|
+
> ⚠️ `cursor` là chuỗi opaque dạng `"<UnixMs>_<ULID>"` (ví dụ: `"1743004200000_01JRZABC1234567890ABCDEF"`). Luôn truyền nguyên dạng string — **KHÔNG được ép kiểu sang number**.
|
|
392
431
|
|
|
393
432
|
### `create(request): Promise<ConversationDto>`
|
|
394
433
|
|
|
@@ -439,17 +478,29 @@ const updated = await client.conversations.update('conv-id', {
|
|
|
439
478
|
});
|
|
440
479
|
```
|
|
441
480
|
|
|
442
|
-
### `
|
|
481
|
+
### `delete(id: string): Promise<void>`
|
|
482
|
+
|
|
483
|
+
Hành vi phụ thuộc vào loại cuộc hội thoại:
|
|
484
|
+
- **Group** → rời nhóm (tương đương `leaveGroup()`). Các participant khác vẫn còn đó.
|
|
485
|
+
- **OneToOne / Self** → ẩn cuộc hội thoại khỏi danh sách (soft-delete). Conversation vẫn tồn tại và sẽ tự khôi phục khi có tin nhắn mới.
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
await client.conversations.delete('conv-id');
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### `leaveGroup(id: string): Promise<void>`
|
|
492
|
+
|
|
493
|
+
Rời cuộc hội thoại **Group** một cách tường minh. Trả về `403 Forbidden` nếu conversation không phải Group.
|
|
443
494
|
|
|
444
|
-
|
|
495
|
+
> Dùng `leaveGroup()` khi cần ngữ nghĩa Group-only với phản hồi lỗi rõ ràng cho non-Group. Dùng `delete()` khi muốn xử lý cả Group lẫn OneToOne/Self trong cùng một flow.
|
|
445
496
|
|
|
446
497
|
```ts
|
|
447
|
-
await client.conversations.
|
|
498
|
+
await client.conversations.leaveGroup('conv-id');
|
|
448
499
|
```
|
|
449
500
|
|
|
450
501
|
### `findOneToOne(otherParticipantId): Promise<ConversationDto | null>`
|
|
451
502
|
|
|
452
|
-
Tìm cuộc hội thoại 1-1 với user khác. Trả về `null` (
|
|
503
|
+
Tìm cuộc hội thoại 1-1 với user khác. Trả về `null` nếu chưa có (server trả 404, SDK chuyển thành `null` để gọi không cần try/catch).
|
|
453
504
|
|
|
454
505
|
```ts
|
|
455
506
|
const existing = await client.conversations.findOneToOne('other-user-id');
|
|
@@ -489,16 +540,49 @@ await client.conversations.pin('conv-id');
|
|
|
489
540
|
await client.conversations.unpin('conv-id');
|
|
490
541
|
```
|
|
491
542
|
|
|
492
|
-
### `markAsRead(id,
|
|
543
|
+
### `markAsRead(id, request) / markAsUnread(id): Promise<void>`
|
|
493
544
|
|
|
494
545
|
```ts
|
|
495
546
|
// Đánh dấu đã đọc đến messageId
|
|
496
|
-
await client.conversations.markAsRead('conv-id', 'last-message-id');
|
|
547
|
+
await client.conversations.markAsRead('conv-id', { messageId: 'last-message-id' });
|
|
497
548
|
|
|
498
549
|
// Đánh dấu chưa đọc
|
|
499
550
|
await client.conversations.markAsUnread('conv-id');
|
|
500
551
|
```
|
|
501
552
|
|
|
553
|
+
### `getMedia(conversationId, params?): Promise<CursorPaginatedResult<ConversationMediaItemDto>>`
|
|
554
|
+
|
|
555
|
+
Liệt kê toàn bộ media (image/video/audio/file) và link (LinkPreview hoặc external URL) đã từng xuất hiện trong cuộc hội thoại — sắp xếp **newest-first**, cursor pagination.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// Mặc định: kind = 'all', limit = 50
|
|
559
|
+
const page1 = await client.conversations.getMedia('conv-id');
|
|
560
|
+
|
|
561
|
+
// Chỉ file/ảnh nội bộ
|
|
562
|
+
const attachments = await client.conversations.getMedia('conv-id', {
|
|
563
|
+
kind: 'attachment',
|
|
564
|
+
limit: 100,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Chỉ link / LinkPreview
|
|
568
|
+
const links = await client.conversations.getMedia('conv-id', { kind: 'link' });
|
|
569
|
+
|
|
570
|
+
// Trang tiếp theo
|
|
571
|
+
if (page1.hasMore) {
|
|
572
|
+
const page2 = await client.conversations.getMedia('conv-id', {
|
|
573
|
+
cursor: page1.nextCursor!,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
| Param | Type | Default | Mô tả |
|
|
579
|
+
|-------|------|---------|-------|
|
|
580
|
+
| `kind` | `'all' \| 'attachment' \| 'link'` | `'all'` | Lọc theo loại item |
|
|
581
|
+
| `limit` | number | 50 | 1..100 |
|
|
582
|
+
| `cursor` | string | — | `nextCursor` từ trang trước (opaque) |
|
|
583
|
+
|
|
584
|
+
> **Lưu ý:** Người dùng chỉ thấy media của các message từ thời điểm họ join cuộc hội thoại trở đi. Server trả `403` nếu caller không phải participant.
|
|
585
|
+
|
|
502
586
|
### Kiểu dữ liệu Conversation
|
|
503
587
|
|
|
504
588
|
```ts
|
|
@@ -507,13 +591,13 @@ type ConversationType = 'Group' | 'OneToOne' | 'Self';
|
|
|
507
591
|
interface ConversationDto {
|
|
508
592
|
id: string;
|
|
509
593
|
type: ConversationType;
|
|
510
|
-
name?: string;
|
|
511
|
-
avatar?: MediaReference;
|
|
594
|
+
name?: string | null; // Tên nhóm (chỉ Group); null cho OneToOne/Self
|
|
595
|
+
avatar?: MediaReference | null;
|
|
512
596
|
ownerId: string;
|
|
513
|
-
lastMessageAt?: string;
|
|
514
|
-
lastMessageId?: string;
|
|
515
|
-
createdAt: string;
|
|
516
|
-
updatedAt: string;
|
|
597
|
+
lastMessageAt?: string | null; // ISO 8601, null khi chưa có tin nhắn
|
|
598
|
+
lastMessageId?: string | null; // null khi chưa có tin nhắn
|
|
599
|
+
createdAt: string; // ISO 8601
|
|
600
|
+
updatedAt: string; // ISO 8601
|
|
517
601
|
participants: ConversationParticipantDto[];
|
|
518
602
|
}
|
|
519
603
|
|
|
@@ -532,16 +616,59 @@ interface ConversationParticipantDto {
|
|
|
532
616
|
interface ConversationListItemDto {
|
|
533
617
|
id: string;
|
|
534
618
|
type: ConversationType;
|
|
535
|
-
name?: string;
|
|
536
|
-
avatar?: MediaReference;
|
|
537
|
-
lastMessageAt?: string;
|
|
538
|
-
lastMessageId?: string;
|
|
539
|
-
lastMessage?: MessageDto;
|
|
619
|
+
name?: string | null; // null cho OneToOne/Self
|
|
620
|
+
avatar?: MediaReference | null;
|
|
621
|
+
lastMessageAt?: string | null; // ISO 8601
|
|
622
|
+
lastMessageId?: string | null;
|
|
623
|
+
lastMessage?: MessageDto | null; // Preview tin nhắn cuối, null khi chưa có tin nhắn
|
|
540
624
|
isPinned: boolean;
|
|
541
|
-
pinnedAt?: string;
|
|
625
|
+
pinnedAt?: string | null; // ISO 8601, null khi chưa pin
|
|
542
626
|
unreadCount: number;
|
|
543
627
|
participantCount: number;
|
|
544
628
|
}
|
|
629
|
+
|
|
630
|
+
// Media listing (getMedia)
|
|
631
|
+
type ConversationMediaKindFilter = 'all' | 'attachment' | 'link';
|
|
632
|
+
type ConversationMediaKind = 'Attachment' | 'Link';
|
|
633
|
+
type ConversationMediaBlockType = 'Image' | 'Video' | 'Audio' | 'File' | 'LinkPreview';
|
|
634
|
+
|
|
635
|
+
interface ConversationMediaItemDto {
|
|
636
|
+
id: string; // Composite "{messageId}#{blockIndex:D2}"
|
|
637
|
+
conversationId: string;
|
|
638
|
+
messageId: string;
|
|
639
|
+
blockIndex: number; // 0-based vị trí block trong message
|
|
640
|
+
senderId: string;
|
|
641
|
+
senderType: string; // 'User' | 'Bot' | 'System'
|
|
642
|
+
kind: ConversationMediaKind;
|
|
643
|
+
blockType: ConversationMediaBlockType;
|
|
644
|
+
createdAt: string; // ISO 8601 — thời điểm message được tạo
|
|
645
|
+
|
|
646
|
+
storageKey?: string | null; // set khi kind === 'Attachment'
|
|
647
|
+
url?: string | null; // set khi kind === 'Link'
|
|
648
|
+
|
|
649
|
+
// File / audio / video
|
|
650
|
+
fileName?: string | null;
|
|
651
|
+
mimeType?: string | null;
|
|
652
|
+
fileSizeBytes?: number | null;
|
|
653
|
+
|
|
654
|
+
// Image / video / audio
|
|
655
|
+
width?: number | null;
|
|
656
|
+
height?: number | null;
|
|
657
|
+
durationSeconds?: number | null;
|
|
658
|
+
caption?: string | null;
|
|
659
|
+
altText?: string | null;
|
|
660
|
+
|
|
661
|
+
// Image / video thumbnail
|
|
662
|
+
thumbnailStorageKey?: string | null;
|
|
663
|
+
thumbnailUrl?: string | null;
|
|
664
|
+
|
|
665
|
+
// LinkPreview metadata
|
|
666
|
+
linkTitle?: string | null;
|
|
667
|
+
linkDescription?: string | null;
|
|
668
|
+
linkSiteName?: string | null;
|
|
669
|
+
linkImageStorageKey?: string | null;
|
|
670
|
+
linkImageUrl?: string | null;
|
|
671
|
+
}
|
|
545
672
|
```
|
|
546
673
|
|
|
547
674
|
---
|
|
@@ -568,7 +695,7 @@ const msg = await client.messages.send('conv-id', {
|
|
|
568
695
|
|
|
569
696
|
> 💡 **REST vs SignalR:** Có thể gửi tin nhắn qua cả REST (`client.messages.send`) lẫn SignalR (`client.realtime.chat.sendMessage`). SignalR trả về ack ngay lập tức và nhanh hơn trong môi trường real-time.
|
|
570
697
|
|
|
571
|
-
### `getHistory(conversationId, params?): Promise<
|
|
698
|
+
### `getHistory(conversationId, params?): Promise<CursorPaginatedResult<MessageDto>>`
|
|
572
699
|
|
|
573
700
|
Lấy lịch sử tin nhắn với cursor-based pagination.
|
|
574
701
|
|
|
@@ -611,11 +738,12 @@ Chỉ người gửi mới có thể chỉnh sửa.
|
|
|
611
738
|
```ts
|
|
612
739
|
const edited = await client.messages.edit('msg-id', {
|
|
613
740
|
blocks: [{ $type: 'text', format: 'Plain', content: 'Nội dung đã sửa', plainText: 'Nội dung đã sửa' }],
|
|
614
|
-
mentions: [],
|
|
741
|
+
mentions: [], // optional
|
|
742
|
+
replyToMessageId: null, // optional — null để xóa reply reference
|
|
615
743
|
});
|
|
616
744
|
```
|
|
617
745
|
|
|
618
|
-
> ⚠️ **Khác biệt REST vs SignalR:** REST dùng `{ blocks, mentions }`, SignalR dùng `{ messageId, newBlocks, newMentions }`.
|
|
746
|
+
> ⚠️ **Khác biệt REST vs SignalR:** REST dùng `{ blocks, mentions, replyToMessageId }`, SignalR dùng `{ messageId, newBlocks, newMentions, newReplyToMessageId }`.
|
|
619
747
|
|
|
620
748
|
### `delete(messageId): Promise<{ messageId, deletedAt }>`
|
|
621
749
|
|
|
@@ -652,7 +780,6 @@ interface MessageDto {
|
|
|
652
780
|
conversationId: string;
|
|
653
781
|
senderId: string;
|
|
654
782
|
senderType: SenderType;
|
|
655
|
-
timestamp: string; // ISO 8601 — thời điểm gửi
|
|
656
783
|
blocks: Block[]; // Nội dung tin nhắn
|
|
657
784
|
plainTextIndex: string | null;
|
|
658
785
|
replyToMessageId: string | null;
|
|
@@ -698,6 +825,7 @@ interface SendMessageRequest {
|
|
|
698
825
|
interface EditMessageRequest {
|
|
699
826
|
blocks: Block[];
|
|
700
827
|
mentions?: Mention[];
|
|
828
|
+
replyToMessageId?: string | null; // null để xóa reply reference
|
|
701
829
|
}
|
|
702
830
|
|
|
703
831
|
// Request đánh dấu đã đọc (REST)
|
|
@@ -755,18 +883,37 @@ const result = await client.files.upload(uploadUrl, file, (loaded, total) => {
|
|
|
755
883
|
|
|
756
884
|
> Khi có `onProgress`, SDK dùng XHR thay vì fetch (vì fetch không hỗ trợ upload progress events).
|
|
757
885
|
|
|
758
|
-
### `getDownloadUrl(storageKey
|
|
886
|
+
### `getDownloadUrl(storageKey, signed?): string`
|
|
759
887
|
|
|
760
888
|
Tạo URL tải file tuyệt đối (dùng để hiển thị ảnh, tải xuống, v.v.).
|
|
761
889
|
|
|
762
890
|
```ts
|
|
891
|
+
// URL public (mặc định)
|
|
763
892
|
const url = client.files.getDownloadUrl('01HXABCDEF/photo.jpg');
|
|
764
893
|
// → "https://chat-api.example.com/api/files/01HXABCDEF/photo.jpg"
|
|
765
894
|
```
|
|
766
895
|
|
|
896
|
+
#### Signed URL (kiểm soát truy cập)
|
|
897
|
+
|
|
898
|
+
Khi server bật signed download (kiểm soát truy cập có thời hạn), truyền `expires` + `sig` ở tham số thứ hai:
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
const url = client.files.getDownloadUrl('01HXABCDEF/photo.jpg', {
|
|
902
|
+
expires: '2026-03-26T13:00:00.0000000Z',
|
|
903
|
+
sig: 'abc123...',
|
|
904
|
+
});
|
|
905
|
+
// → ".../api/files/01HXABCDEF/photo.jpg?expires=...&sig=abc123..."
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
> Server trả `400 Bad Request` (title `Invalid Signature`) nếu URL bị sửa hoặc đã hết hạn. `expires` và `sig` thường lấy từ nguồn server-issued tương ứng (không tự ghép).
|
|
909
|
+
|
|
910
|
+
> **Khi server bật signed download:** Server thường trả sẵn `url` trong `MediaReference` (đã gắn `expires` + `sig`) thay vì yêu cầu client tự gọi `getDownloadUrl(storageKey, { expires, sig })`. Trong trường hợp đó, ưu tiên dùng `MediaReference.url` (sau khi resolve relative — xem [MediaReference](#mediareference)), **không** dùng `getDownloadUrl(storageKey)` vì sẽ tạo ra unsigned URL và bị server từ chối.
|
|
911
|
+
|
|
912
|
+
> **Encoding:** `storageKey` có thể chứa `/` — SDK giữ `/` làm phân cách path; các ký tự đặc biệt khác (space, `?`, `#`, `%`...) tự động được percent-encode theo từng segment.
|
|
913
|
+
|
|
767
914
|
### `delete(storageKey: string): Promise<void>`
|
|
768
915
|
|
|
769
|
-
Xóa file. Chỉ người upload mới có thể xóa. `
|
|
916
|
+
Xóa file. Chỉ người upload mới có thể xóa. Cùng quy tắc encoding như `getDownloadUrl()`: `/` được giữ, ký tự đặc biệt khác tự động percent-encoded.
|
|
770
917
|
|
|
771
918
|
```ts
|
|
772
919
|
await client.files.delete('01HXABCDEF/photo.jpg');
|
|
@@ -807,7 +954,7 @@ GET (`list`, `getById`) là **public**. Các thao tác quản lý yêu cầu JWT
|
|
|
807
954
|
|
|
808
955
|
### `list(params?): Promise<PagedResult<BotDto>>`
|
|
809
956
|
|
|
810
|
-
> Lưu ý: BotApi dùng **page-based** pagination (không phải cursor-based). `page` bắt đầu từ 1.
|
|
957
|
+
> Lưu ý: BotApi dùng **page-based** pagination (không phải cursor-based). `page` bắt đầu từ 1.
|
|
811
958
|
|
|
812
959
|
```ts
|
|
813
960
|
const result = await client.bots.list({ page: 1, pageSize: 20 });
|
|
@@ -817,17 +964,15 @@ console.log(result.totalCount); // Tổng số bot
|
|
|
817
964
|
|
|
818
965
|
### `getById(id): Promise<BotDto>`
|
|
819
966
|
|
|
820
|
-
> Server cache kết quả **5 phút**.
|
|
821
|
-
|
|
822
967
|
```ts
|
|
823
968
|
const bot = await client.bots.getById('bot-id');
|
|
824
969
|
// Lấy thêm thông tin display: name, avatar
|
|
825
970
|
const participant = await client.participants.getById(bot.participantId);
|
|
826
971
|
```
|
|
827
972
|
|
|
828
|
-
### `create(request): Promise<BotDto
|
|
973
|
+
### `create(request): Promise<BotDto>`
|
|
829
974
|
|
|
830
|
-
|
|
975
|
+
Đăng ký bot mới.
|
|
831
976
|
|
|
832
977
|
```ts
|
|
833
978
|
const bot = await client.bots.create({
|
|
@@ -835,10 +980,8 @@ const bot = await client.bots.create({
|
|
|
835
980
|
fullName: 'My Assistant Bot',
|
|
836
981
|
description: 'Trợ lý AI',
|
|
837
982
|
kafkaTopic: 'my-bot-topic', // Không thể thay đổi sau khi tạo
|
|
838
|
-
rateLimitPerSecond: 5,
|
|
839
983
|
listenAllGroupMessages: false,
|
|
840
984
|
});
|
|
841
|
-
const apiKey = bot.apiKey; // Lưu ngay!
|
|
842
985
|
```
|
|
843
986
|
|
|
844
987
|
### `update(id, request): Promise<BotDto>`
|
|
@@ -846,7 +989,6 @@ const apiKey = bot.apiKey; // Lưu ngay!
|
|
|
846
989
|
```ts
|
|
847
990
|
await client.bots.update('bot-id', {
|
|
848
991
|
fullName: 'Updated Bot Name',
|
|
849
|
-
rateLimitPerSecond: 10,
|
|
850
992
|
// kafkaTopic KHÔNG thể cập nhật
|
|
851
993
|
});
|
|
852
994
|
```
|
|
@@ -858,14 +1000,6 @@ await client.bots.activate('bot-id');
|
|
|
858
1000
|
await client.bots.deactivate('bot-id');
|
|
859
1001
|
```
|
|
860
1002
|
|
|
861
|
-
### `regenerateKey(id): Promise<RegenerateKeyResponse>`
|
|
862
|
-
|
|
863
|
-
Xoay API key — key cũ bị vô hiệu hóa ngay lập tức.
|
|
864
|
-
|
|
865
|
-
```ts
|
|
866
|
-
const { apiKey } = await client.bots.regenerateKey('bot-id');
|
|
867
|
-
```
|
|
868
|
-
|
|
869
1003
|
### `delete(id): Promise<void>`
|
|
870
1004
|
|
|
871
1005
|
Soft delete.
|
|
@@ -880,11 +1014,10 @@ await client.bots.delete('bot-id');
|
|
|
880
1014
|
interface BotDto {
|
|
881
1015
|
id: string;
|
|
882
1016
|
participantId: string; // Dùng để lấy name/avatar từ ParticipantApi
|
|
883
|
-
description?: string;
|
|
884
|
-
metadata?: Record<string, unknown
|
|
1017
|
+
description?: string | null;
|
|
1018
|
+
metadata?: Record<string, unknown> | null;
|
|
885
1019
|
kafkaTopic: string; // KHÔNG thể thay đổi sau khi tạo
|
|
886
1020
|
isActive: boolean;
|
|
887
|
-
rateLimitPerSecond: number;
|
|
888
1021
|
listenAllGroupMessages: boolean;
|
|
889
1022
|
createdBy: string; // ID của người tạo bot
|
|
890
1023
|
createdAt: string; // ISO 8601
|
|
@@ -904,7 +1037,6 @@ interface CreateBotRequest {
|
|
|
904
1037
|
description?: string;
|
|
905
1038
|
metadata?: Record<string, unknown>;
|
|
906
1039
|
kafkaTopic: string; // KHÔNG thể thay đổi sau khi tạo
|
|
907
|
-
rateLimitPerSecond?: number;
|
|
908
1040
|
listenAllGroupMessages?: boolean; // Mặc định false
|
|
909
1041
|
}
|
|
910
1042
|
|
|
@@ -914,7 +1046,6 @@ interface UpdateBotRequest {
|
|
|
914
1046
|
avatar?: MediaReference;
|
|
915
1047
|
description?: string;
|
|
916
1048
|
metadata?: Record<string, unknown>;
|
|
917
|
-
rateLimitPerSecond?: number;
|
|
918
1049
|
listenAllGroupMessages?: boolean;
|
|
919
1050
|
// kafkaTopic KHÔNG thể cập nhật
|
|
920
1051
|
}
|
|
@@ -938,7 +1069,34 @@ const order = await client.proxy.post<Order>('trading', 'api/orders', {
|
|
|
938
1069
|
quantity: 100,
|
|
939
1070
|
});
|
|
940
1071
|
|
|
941
|
-
//
|
|
1072
|
+
// PUT /api/proxy/trading/api/orders/123
|
|
1073
|
+
const updated = await client.proxy.put<Order>('trading', 'api/orders/123', {
|
|
1074
|
+
quantity: 200,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// PATCH /api/proxy/trading/api/orders/123
|
|
1078
|
+
const patched = await client.proxy.patch<Order>('trading', 'api/orders/123', {
|
|
1079
|
+
status: 'cancelled',
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// DELETE /api/proxy/trading/api/orders/123
|
|
1083
|
+
await client.proxy.delete('trading', 'api/orders/123');
|
|
1084
|
+
|
|
1085
|
+
// Truyền custom headers — mỗi shortcut đều nhận `headers?` ở tham số cuối
|
|
1086
|
+
const dataWithHeader = await client.proxy.get<StockData>(
|
|
1087
|
+
'trading',
|
|
1088
|
+
'api/stocks/VN30',
|
|
1089
|
+
{ 'X-Trace-Id': 'abc-123' },
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
const orderWithHeader = await client.proxy.post<Order>(
|
|
1093
|
+
'trading',
|
|
1094
|
+
'api/orders',
|
|
1095
|
+
{ symbol: 'VNM', quantity: 100 },
|
|
1096
|
+
{ 'Idempotency-Key': 'order-uuid' },
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
// Request tùy chỉnh method (hỗ trợ GET, POST, PUT, PATCH, DELETE)
|
|
942
1100
|
const result = await client.proxy.request<unknown>('myservice', 'api/resource/123', {
|
|
943
1101
|
method: 'DELETE',
|
|
944
1102
|
headers: { 'X-Custom-Header': 'value' },
|
|
@@ -1034,6 +1192,12 @@ await client.realtime.chat.leaveConversation('conv-id');
|
|
|
1034
1192
|
```
|
|
1035
1193
|
|
|
1036
1194
|
> SDK theo dõi `joinedConversations` và tự động re-join sau khi reconnect.
|
|
1195
|
+
>
|
|
1196
|
+
> **Lưu ý về FORBIDDEN/UNAUTHORIZED:** Theo spec, server có thể chấp nhận lời gọi `joinConversation` nhưng từ chối quyền truy cập sau đó qua event `Error` (vd. `FORBIDDEN — You are not a participant`). SDK sẽ tự động loại conversationId đó khỏi `joinedConversations` để tránh re-join sai sau reconnect, dựa trên:
|
|
1197
|
+
> 1. `error.details.conversationId` nếu server cung cấp (chính xác nhất), hoặc
|
|
1198
|
+
> 2. Fallback: message khớp `/not a participant/i` **và** chỉ có đúng 1 lời gọi `joinConversation` đang trong cửa sổ ~2s.
|
|
1199
|
+
>
|
|
1200
|
+
> Vẫn nên đăng ký listener `error` để hiển thị thông báo cho user.
|
|
1037
1201
|
|
|
1038
1202
|
### Gửi tin nhắn qua SignalR
|
|
1039
1203
|
|
|
@@ -1054,10 +1218,11 @@ if (ack.success) {
|
|
|
1054
1218
|
### Chỉnh sửa / Xóa / Khôi phục tin nhắn qua SignalR
|
|
1055
1219
|
|
|
1056
1220
|
```ts
|
|
1057
|
-
// Chỉnh sửa — dùng newBlocks/newMentions (khác REST!)
|
|
1221
|
+
// Chỉnh sửa — dùng newBlocks/newMentions/newReplyToMessageId (khác REST!)
|
|
1058
1222
|
const editAck = await client.realtime.chat.editMessage({
|
|
1059
1223
|
messageId: 'msg-id',
|
|
1060
1224
|
newBlocks: [{ $type: 'text', format: 'Plain', content: 'Đã sửa', plainText: 'Đã sửa' }],
|
|
1225
|
+
newReplyToMessageId: null, // optional — null để xóa reply reference
|
|
1061
1226
|
});
|
|
1062
1227
|
|
|
1063
1228
|
// Xóa — truyền object, KHÔNG phải string
|
|
@@ -1112,6 +1277,7 @@ client.realtime.chat.off('messageReceived', handler);
|
|
|
1112
1277
|
| `messageUpdated` | `MessageUpdatedDto` | Tin nhắn được chỉnh sửa |
|
|
1113
1278
|
| `messageDeleted` | `MessageDeletedDto` | Tin nhắn bị xóa |
|
|
1114
1279
|
| `messageRecovered` | `ChatMessageDto` | Tin nhắn được khôi phục |
|
|
1280
|
+
| `messageThumbnailsReady` | `MessageThumbnailsReadyDto` | Server đã sinh xong thumbnail cho ảnh/video — patch theo `blockIndex` |
|
|
1115
1281
|
| `reactionAdded` | `ReactionAddedDto` | Thêm reaction |
|
|
1116
1282
|
| `reactionRemoved` | `ReactionRemovedDto` | Xóa reaction |
|
|
1117
1283
|
| `typingStarted` | `TypingDto` | Người dùng đang gõ |
|
|
@@ -1137,7 +1303,8 @@ interface MessageUpdatedDto {
|
|
|
1137
1303
|
blocks: Block[];
|
|
1138
1304
|
plainTextContent: string | null;
|
|
1139
1305
|
mentions?: Mention[];
|
|
1140
|
-
updatedAt: string;
|
|
1306
|
+
updatedAt: string; // ISO 8601
|
|
1307
|
+
replyToMessageId?: string | null; // null nếu reply reference đã bị xóa
|
|
1141
1308
|
}
|
|
1142
1309
|
|
|
1143
1310
|
// Tin nhắn bị xóa
|
|
@@ -1185,6 +1352,36 @@ interface ReadReceiptUpdatedDto {
|
|
|
1185
1352
|
lastReadMessageId: string;
|
|
1186
1353
|
readAt: string; // ISO 8601
|
|
1187
1354
|
}
|
|
1355
|
+
|
|
1356
|
+
// Thumbnail ready cho một block media trong message
|
|
1357
|
+
interface MessageThumbnailDto {
|
|
1358
|
+
blockIndex: number; // Vị trí block trong mảng blocks của message
|
|
1359
|
+
thumbnail: MediaReference;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Server đã sinh xong thumbnail cho ảnh/video (xử lý bất đồng bộ).
|
|
1363
|
+
// Event này gửi SAU messageReceived/streamCompleted.
|
|
1364
|
+
interface MessageThumbnailsReadyDto {
|
|
1365
|
+
messageId: string;
|
|
1366
|
+
conversationId: string;
|
|
1367
|
+
thumbnails: MessageThumbnailDto[];
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
### Cập nhật thumbnail bất đồng bộ
|
|
1372
|
+
|
|
1373
|
+
Khi gửi tin nhắn có ảnh/video, server xử lý thumbnail trong nền và phát event `messageThumbnailsReady` sau khi hoàn tất. Client nên patch block theo `blockIndex` thay vì re-fetch toàn bộ message.
|
|
1374
|
+
|
|
1375
|
+
```ts
|
|
1376
|
+
client.realtime.chat.on('messageThumbnailsReady', (dto) => {
|
|
1377
|
+
// dto.thumbnails: [{ blockIndex: 0, thumbnail: { storageKey, url } }, ...]
|
|
1378
|
+
for (const item of dto.thumbnails) {
|
|
1379
|
+
const block = messageStore.get(dto.messageId)?.blocks[item.blockIndex];
|
|
1380
|
+
if (block && (block.$type === 'image' || block.$type === 'video')) {
|
|
1381
|
+
block.thumbnail = item.thumbnail;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1188
1385
|
```
|
|
1189
1386
|
|
|
1190
1387
|
### Bot streaming
|
|
@@ -1262,7 +1459,7 @@ await client.realtime.notifications.resubscribePresence();
|
|
|
1262
1459
|
### Theo dõi trạng thái online (Presence)
|
|
1263
1460
|
|
|
1264
1461
|
```ts
|
|
1265
|
-
// Đăng ký theo dõi
|
|
1462
|
+
// Đăng ký theo dõi presence — SDK tự chia nhỏ thành các batch 200 nếu danh sách dài hơn
|
|
1266
1463
|
await client.realtime.notifications.subscribeToPresence(['user-1', 'user-2', 'user-3']);
|
|
1267
1464
|
|
|
1268
1465
|
// Server gửi PresenceState ngay lập tức sau khi subscribe
|
|
@@ -1277,7 +1474,7 @@ client.realtime.notifications.on('presenceChanged', (dto) => {
|
|
|
1277
1474
|
console.log(dto.participantId, 'is now', dto.isOnline ? 'online' : 'offline');
|
|
1278
1475
|
});
|
|
1279
1476
|
|
|
1280
|
-
// Hủy theo dõi — cũng
|
|
1477
|
+
// Hủy theo dõi — cũng tự chia nhỏ thành các batch 200
|
|
1281
1478
|
await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
1282
1479
|
```
|
|
1283
1480
|
|
|
@@ -1299,6 +1496,8 @@ await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
|
1299
1496
|
| `conversationPinned` | `ConversationPinnedDto` | Cuộc hội thoại được pin |
|
|
1300
1497
|
| `conversationUnpinned` | `ConversationUnpinnedDto` | Cuộc hội thoại được unpin |
|
|
1301
1498
|
| `readStatusChanged` | `ReadStatusChangedDto` | Trạng thái đọc thay đổi |
|
|
1499
|
+
| `conversationDeleted` | `ConversationDeletedDto` | Conversation bị ẩn/xóa khỏi danh sách (chỉ OneToOne & Self, chỉ user thực hiện) |
|
|
1500
|
+
| `conversationRestored` | `ConversationRestoredDto` | Conversation bị ẩn được khôi phục tự động do có tin nhắn mới (chỉ OneToOne & Self) |
|
|
1302
1501
|
| `error` | `HubErrorDto` | Lỗi từ server |
|
|
1303
1502
|
| `reconnecting` | `Error \| undefined` | Đang reconnect |
|
|
1304
1503
|
| `reconnected` | `string \| undefined` | Đã reconnect |
|
|
@@ -1311,24 +1510,24 @@ await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
|
1311
1510
|
interface PresenceStateItem {
|
|
1312
1511
|
participantId: string;
|
|
1313
1512
|
isOnline: boolean;
|
|
1314
|
-
lastSeenAt?: string;
|
|
1513
|
+
lastSeenAt?: string | null; // ISO 8601, null khi đang online
|
|
1315
1514
|
}
|
|
1316
1515
|
|
|
1317
1516
|
// Thay đổi trạng thái online — real-time update
|
|
1318
1517
|
interface PresenceChangedDto {
|
|
1319
1518
|
participantId: string;
|
|
1320
1519
|
isOnline: boolean;
|
|
1321
|
-
lastSeenAt?: string;
|
|
1520
|
+
lastSeenAt?: string | null; // ISO 8601, null khi đang online
|
|
1322
1521
|
}
|
|
1323
1522
|
|
|
1324
1523
|
interface NewMessageNotificationDto {
|
|
1325
1524
|
conversationId: string;
|
|
1326
1525
|
conversationType: ConversationType;
|
|
1327
|
-
conversationName?: string;
|
|
1526
|
+
conversationName?: string | null;
|
|
1328
1527
|
messageId: string;
|
|
1329
1528
|
senderId: string;
|
|
1330
1529
|
senderName: string;
|
|
1331
|
-
senderAvatar?: MediaReference;
|
|
1530
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1332
1531
|
contentPreview: string;
|
|
1333
1532
|
sentAt: string; // ISO 8601
|
|
1334
1533
|
}
|
|
@@ -1353,15 +1552,15 @@ interface UnreadCountChangedDto {
|
|
|
1353
1552
|
interface ConversationCreatedDto {
|
|
1354
1553
|
conversationId: string;
|
|
1355
1554
|
type: ConversationType;
|
|
1356
|
-
name?: string;
|
|
1357
|
-
avatar?: MediaReference;
|
|
1555
|
+
name?: string | null;
|
|
1556
|
+
avatar?: MediaReference | null;
|
|
1358
1557
|
participantCount: number;
|
|
1359
1558
|
}
|
|
1360
1559
|
|
|
1361
1560
|
interface ConversationUpdatedDto {
|
|
1362
1561
|
conversationId: string;
|
|
1363
1562
|
type: ConversationType;
|
|
1364
|
-
name?: string;
|
|
1563
|
+
name?: string | null;
|
|
1365
1564
|
avatar?: MediaReference | null; // null = avatar đã bị xóa
|
|
1366
1565
|
participantCount: number;
|
|
1367
1566
|
}
|
|
@@ -1370,7 +1569,7 @@ interface ParticipantJoinedDto {
|
|
|
1370
1569
|
conversationId: string;
|
|
1371
1570
|
participantId: string;
|
|
1372
1571
|
participantName: string;
|
|
1373
|
-
participantAvatar?: MediaReference;
|
|
1572
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1374
1573
|
changedAt: string; // ISO 8601
|
|
1375
1574
|
}
|
|
1376
1575
|
|
|
@@ -1379,7 +1578,7 @@ interface ParticipantLeftDto {
|
|
|
1379
1578
|
conversationId: string;
|
|
1380
1579
|
participantId: string;
|
|
1381
1580
|
participantName: string;
|
|
1382
|
-
participantAvatar?: MediaReference;
|
|
1581
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1383
1582
|
changedAt: string; // ISO 8601
|
|
1384
1583
|
}
|
|
1385
1584
|
|
|
@@ -1399,6 +1598,26 @@ interface ReadStatusChangedDto {
|
|
|
1399
1598
|
unreadCount: number;
|
|
1400
1599
|
markedAsUnread: boolean; // true khi user chủ động đánh dấu chưa đọc
|
|
1401
1600
|
}
|
|
1601
|
+
|
|
1602
|
+
// Conversation bị ẩn/xóa khỏi danh sách của user.
|
|
1603
|
+
// Chỉ áp dụng cho OneToOne và Self — không broadcast tới người còn lại.
|
|
1604
|
+
// Conversation vẫn tồn tại và sẽ tự khôi phục khi có tin nhắn mới.
|
|
1605
|
+
interface ConversationDeletedDto {
|
|
1606
|
+
conversationId: string;
|
|
1607
|
+
deletedAt: string; // ISO 8601
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Conversation đã bị ẩn được khôi phục tự động vì có tin nhắn mới.
|
|
1611
|
+
// Client nên thêm lại conversation vào sidebar với metadata đầy đủ từ payload.
|
|
1612
|
+
// Chú ý: Ngay sau event này, client cũng sẽ nhận newMessageNotification cho tin nhắn kích hoạt khôi phục.
|
|
1613
|
+
interface ConversationRestoredDto {
|
|
1614
|
+
conversationId: string;
|
|
1615
|
+
type: ConversationType;
|
|
1616
|
+
name?: string | null;
|
|
1617
|
+
avatar?: MediaReference | null;
|
|
1618
|
+
participantCount: number;
|
|
1619
|
+
restoredAt: string; // ISO 8601
|
|
1620
|
+
}
|
|
1402
1621
|
```
|
|
1403
1622
|
|
|
1404
1623
|
---
|
|
@@ -1408,7 +1627,7 @@ interface ReadStatusChangedDto {
|
|
|
1408
1627
|
Class quản lý reconnect thủ công khi cả `withAutomaticReconnect` của SignalR đã thất bại và hub bị `disconnected`. Cung cấp exponential backoff và xử lý token hết hạn.
|
|
1409
1628
|
|
|
1410
1629
|
```ts
|
|
1411
|
-
import { ReconnectionManager } from 'chat-client-sdk';
|
|
1630
|
+
import { ReconnectionManager } from '@manonero/chat-client-sdk';
|
|
1412
1631
|
|
|
1413
1632
|
const manager = new ReconnectionManager({
|
|
1414
1633
|
chatHub: client.realtime.chat,
|
|
@@ -1417,7 +1636,7 @@ const manager = new ReconnectionManager({
|
|
|
1417
1636
|
// Gọi API refresh token của ứng dụng
|
|
1418
1637
|
const newToken = await refreshToken();
|
|
1419
1638
|
if (newToken) {
|
|
1420
|
-
client.setToken(newToken); // Cập nhật token vào SDK
|
|
1639
|
+
client.setToken(newToken); // Cập nhật token vào SDK; lần connect() kế tiếp sẽ dùng giá trị mới
|
|
1421
1640
|
}
|
|
1422
1641
|
return newToken; // Trả về null để hủy reconnect
|
|
1423
1642
|
},
|
|
@@ -1431,7 +1650,9 @@ manager.stop();
|
|
|
1431
1650
|
|
|
1432
1651
|
**Chiến lược backoff:** 3 lần thử với delay 2s → 5s → 10s.
|
|
1433
1652
|
|
|
1434
|
-
**Phát hiện token hết hạn:**
|
|
1653
|
+
**Phát hiện token hết hạn:** heuristic — match (case-insensitive) các keyword `401`, `unauthorized`, `expired`, `invalid_token`, `invalid token` trong message của error.
|
|
1654
|
+
|
|
1655
|
+
**Bỏ qua close chủ động:** Cả `ChatHubClient` và `NotificationHubClient` expose getter `intentionallyClosed`. Trước khi tear down trong `disconnect()` (kể cả khi gọi qua `client.disconnect()` / `client.logout()`), cờ này được set `true`. Manager kiểm tra cờ ngay đầu handler và **bỏ qua** lần `disconnected` đó — không tự reconnect ngược ý người dùng. Cờ được reset về `false` ở đầu lần `connect()` kế tiếp.
|
|
1435
1656
|
|
|
1436
1657
|
---
|
|
1437
1658
|
|
|
@@ -1577,7 +1798,7 @@ interface CustomBlock {
|
|
|
1577
1798
|
SDK xuất sẵn các hàm kiểm tra kiểu (type guard), giúp TypeScript thu hẹp kiểu tự động:
|
|
1578
1799
|
|
|
1579
1800
|
```ts
|
|
1580
|
-
import { isTextBlock, isImageBlock, isFileBlock, isCardBlock } from 'chat-client-sdk';
|
|
1801
|
+
import { isTextBlock, isImageBlock, isFileBlock, isCardBlock } from '@manonero/chat-client-sdk';
|
|
1581
1802
|
|
|
1582
1803
|
for (const block of message.blocks) {
|
|
1583
1804
|
if (isTextBlock(block)) {
|
|
@@ -1645,7 +1866,7 @@ type ButtonStyle = 'Default' | 'Primary' | 'Danger';
|
|
|
1645
1866
|
interface ActionButton {
|
|
1646
1867
|
label: string;
|
|
1647
1868
|
action: ButtonAction;
|
|
1648
|
-
value
|
|
1869
|
+
value?: string | null;
|
|
1649
1870
|
style: ButtonStyle;
|
|
1650
1871
|
}
|
|
1651
1872
|
|
|
@@ -1704,12 +1925,12 @@ interface Mention {
|
|
|
1704
1925
|
|
|
1705
1926
|
## 16. Toàn bộ kiểu dữ liệu (Types)
|
|
1706
1927
|
|
|
1707
|
-
###
|
|
1928
|
+
### CursorPaginatedResult (cursor-based)
|
|
1708
1929
|
|
|
1709
1930
|
```ts
|
|
1710
|
-
interface
|
|
1931
|
+
interface CursorPaginatedResult<T> {
|
|
1711
1932
|
items: T[];
|
|
1712
|
-
nextCursor: string | null; // PHẢI là string — KHÔNG cast sang number
|
|
1933
|
+
nextCursor: string | null; // PHẢI là string opaque — KHÔNG cast sang number
|
|
1713
1934
|
hasMore: boolean;
|
|
1714
1935
|
}
|
|
1715
1936
|
```
|
|
@@ -1729,8 +1950,34 @@ interface PagedResult<T> {
|
|
|
1729
1950
|
|
|
1730
1951
|
```ts
|
|
1731
1952
|
interface MediaReference {
|
|
1732
|
-
storageKey?: string; // Key nội bộ — dùng FileApi.getDownloadUrl() để tạo URL
|
|
1733
|
-
url?: string; // URL
|
|
1953
|
+
storageKey?: string; // Key lưu trữ nội bộ — dùng FileApi.getDownloadUrl(storageKey) để tạo URL tuyệt đối
|
|
1954
|
+
url?: string; // URL do server cấp sẵn (xem chi tiết bên dưới)
|
|
1955
|
+
}
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
Trường `url` có hai dạng tuỳ cấu hình server:
|
|
1959
|
+
|
|
1960
|
+
| Dạng | Ví dụ | Cách dùng |
|
|
1961
|
+
|------|-------|-----------|
|
|
1962
|
+
| **Relative signed URL** | `/api/files/abc/photo.jpg?expires=...&sig=...` | Phải prepend `baseUrl` trước khi dùng |
|
|
1963
|
+
| **Absolute external URL** | `https://cdn.example.com/photo.jpg` | Dùng trực tiếp |
|
|
1964
|
+
|
|
1965
|
+
> **Quy tắc resolve:** Kiểm tra `url` trước, `storageKey` sau. Nếu `url` bắt đầu bằng `/`, đây là relative signed URL — cần ghép `baseUrl`. Tuyệt đối **không** bỏ qua `url` để dùng `storageKey` vì signed URL có thể bắt buộc (server trả `400 Invalid Signature` nếu gọi bằng unsigned URL).
|
|
1966
|
+
|
|
1967
|
+
```ts
|
|
1968
|
+
// Helper chuẩn để resolve MediaReference → URL tuyệt đối
|
|
1969
|
+
function resolveMediaRef(
|
|
1970
|
+
ref: MediaReference | null | undefined,
|
|
1971
|
+
baseUrl: string,
|
|
1972
|
+
getDownloadUrl: (key: string) => string,
|
|
1973
|
+
): string | null {
|
|
1974
|
+
if (!ref) return null;
|
|
1975
|
+
if (ref.url) {
|
|
1976
|
+
// Relative signed URL → prepend baseUrl
|
|
1977
|
+
return ref.url.startsWith('/') ? baseUrl.replace(/\/$/, '') + ref.url : ref.url;
|
|
1978
|
+
}
|
|
1979
|
+
if (ref.storageKey) return getDownloadUrl(ref.storageKey);
|
|
1980
|
+
return null;
|
|
1734
1981
|
}
|
|
1735
1982
|
```
|
|
1736
1983
|
|
|
@@ -1744,7 +1991,7 @@ interface ChatMessageDto {
|
|
|
1744
1991
|
conversationId: string;
|
|
1745
1992
|
senderId: string;
|
|
1746
1993
|
senderName: string; // Có ở SignalR, không có ở REST
|
|
1747
|
-
senderAvatar?: MediaReference; // Có ở SignalR, không có ở REST
|
|
1994
|
+
senderAvatar?: MediaReference | null; // Có ở SignalR, không có ở REST. Có thể null khi server chưa sẵn signed URL
|
|
1748
1995
|
senderType: SenderType; // Có ở cả REST và SignalR
|
|
1749
1996
|
blocks: Block[];
|
|
1750
1997
|
plainTextContent: string | null; // Tên khác với REST (plainTextIndex)
|
|
@@ -1777,13 +2024,15 @@ type SystemEventType =
|
|
|
1777
2024
|
| 'MemberAdded' // Thêm thành viên
|
|
1778
2025
|
| 'MemberRemoved' // Xóa thành viên
|
|
1779
2026
|
| 'MemberLeft' // Thành viên tự rời
|
|
1780
|
-
| 'Custom';
|
|
2027
|
+
| 'Custom'; // Sự kiện tùy chỉnh do server định nghĩa
|
|
1781
2028
|
|
|
1782
2029
|
interface SystemEventInfo {
|
|
1783
2030
|
type: SystemEventType;
|
|
1784
|
-
actorId
|
|
2031
|
+
actorId?: string | null; // null/undefined khi không có actor (vd. MemberLeft tự rời)
|
|
1785
2032
|
targetIds?: string[];
|
|
1786
|
-
|
|
2033
|
+
// Tất cả giá trị đều là string (vd. ConversationRenamed → { newName: "Team Beta" }).
|
|
2034
|
+
// Server có thể trả null cho event không kèm metadata (vd. MemberAdded thuần).
|
|
2035
|
+
metadata?: Record<string, string> | null;
|
|
1787
2036
|
}
|
|
1788
2037
|
|
|
1789
2038
|
// SignalR (ChatHub) version — kế thừa SystemEventInfo, thêm display names
|
|
@@ -1799,7 +2048,8 @@ interface SystemEventDto extends SystemEventInfo {
|
|
|
1799
2048
|
interface HubErrorDto {
|
|
1800
2049
|
code: string;
|
|
1801
2050
|
message: string;
|
|
1802
|
-
|
|
2051
|
+
// Server có thể gửi `null` rõ ràng, hoặc bỏ field hoàn toàn
|
|
2052
|
+
details?: Record<string, unknown> | null;
|
|
1803
2053
|
}
|
|
1804
2054
|
```
|
|
1805
2055
|
|
|
@@ -1812,8 +2062,8 @@ interface StreamStartedDto {
|
|
|
1812
2062
|
conversationId: string;
|
|
1813
2063
|
senderId: string;
|
|
1814
2064
|
senderName: string;
|
|
1815
|
-
senderAvatar?: MediaReference;
|
|
1816
|
-
replyToMessageId?: string;
|
|
2065
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
2066
|
+
replyToMessageId?: string | null;
|
|
1817
2067
|
startedAt: string; // ISO 8601
|
|
1818
2068
|
}
|
|
1819
2069
|
|
|
@@ -1860,7 +2110,7 @@ interface SendMessageAck {
|
|
|
1860
2110
|
success: boolean;
|
|
1861
2111
|
messageId?: string;
|
|
1862
2112
|
clientMessageId?: string;
|
|
1863
|
-
|
|
2113
|
+
createdAt?: string; // ISO 8601 — thời điểm server tạo tin nhắn
|
|
1864
2114
|
errorCode?: string;
|
|
1865
2115
|
errorMessage?: string;
|
|
1866
2116
|
}
|
|
@@ -1911,7 +2161,7 @@ interface MarkAsReadAck {
|
|
|
1911
2161
|
Tất cả lỗi HTTP đều throw `ChatApiError` (kế thừa `Error`).
|
|
1912
2162
|
|
|
1913
2163
|
```ts
|
|
1914
|
-
import { ChatApiError } from 'chat-client-sdk';
|
|
2164
|
+
import { ChatApiError } from '@manonero/chat-client-sdk';
|
|
1915
2165
|
|
|
1916
2166
|
try {
|
|
1917
2167
|
await client.messages.send('conv-id', { blocks: [...] });
|
|
@@ -1919,7 +2169,17 @@ try {
|
|
|
1919
2169
|
if (err instanceof ChatApiError) {
|
|
1920
2170
|
console.error(`HTTP ${err.status}: ${err.title}`);
|
|
1921
2171
|
console.error('Chi tiết:', err.detail);
|
|
1922
|
-
|
|
2172
|
+
|
|
2173
|
+
// Xem tất cả lỗi validation chi tiết
|
|
2174
|
+
if (err.errors) {
|
|
2175
|
+
err.errors.forEach(e => console.error(`[${e.code}] ${e.description}`));
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Dùng traceId để correlate log
|
|
2179
|
+
if (err.traceId) {
|
|
2180
|
+
console.error('TraceId:', err.traceId);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
1923
2183
|
switch (err.status) {
|
|
1924
2184
|
case 401: // Unauthorized — token hết hạn/không hợp lệ
|
|
1925
2185
|
await refreshAndRetry();
|
|
@@ -1942,12 +2202,17 @@ try {
|
|
|
1942
2202
|
| Thuộc tính | Kiểu | Mô tả |
|
|
1943
2203
|
|------------|------|-------|
|
|
1944
2204
|
| `status` | `number` | HTTP status code |
|
|
1945
|
-
| `title` | `string` | Tiêu đề lỗi
|
|
1946
|
-
| `detail` | `string \| undefined` |
|
|
2205
|
+
| `title` | `string` | Tiêu đề lỗi — giá trị `ErrorType` enum: `Validation`, `NotFound`, `Conflict`, `Forbidden`, `Unauthorized`, `Unexpected`. Auth endpoints trả `Bad Request`, v.v. |
|
|
2206
|
+
| `detail` | `string \| undefined` | Mô tả của lỗi đầu tiên (từ RFC 7807 `detail`) |
|
|
2207
|
+
| `type` | `string \| undefined` | URI tham chiếu RFC 7807 `type` (nếu server gửi). `undefined` cho proxy errors và lỗi sinh nội bộ (timeout/network/parse) |
|
|
2208
|
+
| `errors` | `ProblemError[] \| undefined` | Mảng tất cả lỗi chi tiết — có khi lỗi đi qua handler (ErrorOr path). `undefined` cho lỗi middleware/filter, proxy errors, và lỗi sinh nội bộ |
|
|
2209
|
+
| `traceId` | `string \| undefined` | W3C trace ID để correlate log. Tự động thêm bởi framework. `undefined` cho proxy errors và lỗi sinh nội bộ |
|
|
1947
2210
|
| `message` | `string` | `detail ?? title` (kế thừa từ `Error`) |
|
|
1948
2211
|
| `name` | `string` | Luôn là `"ChatApiError"` |
|
|
1949
2212
|
|
|
1950
|
-
Server trả lỗi dạng RFC 7807: `{ status, title, detail }`. Endpoint proxy trả `{ detail }` đơn giản hơn — SDK xử lý cả hai.
|
|
2213
|
+
Server trả lỗi dạng RFC 7807: `{ type?, status, title, detail?, errors?, traceId? }`. Endpoint proxy trả `{ detail }` đơn giản hơn — SDK xử lý cả hai.
|
|
2214
|
+
|
|
2215
|
+
> **Lưu ý:** `errors` chỉ có mặt khi lỗi đi qua handler (ErrorOr path → `ToProblemResult`). Lỗi từ middleware/filter (401 thiếu token, lỗi routing) trả ProblemDetails **không có** trường `errors`.
|
|
1951
2216
|
|
|
1952
2217
|
### Static methods
|
|
1953
2218
|
|
|
@@ -1960,8 +2225,8 @@ Parse lỗi từ HTTP `Response`. SDK gọi nội bộ — bạn không cần g
|
|
|
1960
2225
|
Tạo `ChatApiError` từ object `ProblemDetails` (RFC 7807). Hữu ích khi bạn nhận `ProblemDetails` từ nguồn khác (ví dụ: WebSocket, tự parse).
|
|
1961
2226
|
|
|
1962
2227
|
```ts
|
|
1963
|
-
import { ChatApiError } from 'chat-client-sdk';
|
|
1964
|
-
import type { ProblemDetails } from 'chat-client-sdk';
|
|
2228
|
+
import { ChatApiError } from '@manonero/chat-client-sdk';
|
|
2229
|
+
import type { ProblemDetails } from '@manonero/chat-client-sdk';
|
|
1965
2230
|
|
|
1966
2231
|
const pd: ProblemDetails = { status: 404, title: 'Not Found', detail: 'Conversation not found' };
|
|
1967
2232
|
const error = ChatApiError.fromProblemDetails(pd);
|
|
@@ -1978,7 +2243,16 @@ interface ProblemDetails {
|
|
|
1978
2243
|
status: number;
|
|
1979
2244
|
title: string;
|
|
1980
2245
|
detail?: string;
|
|
1981
|
-
type?: string;
|
|
2246
|
+
type?: string; // URI tham chiếu (RFC 7807) — `ChatApiError.fromProblemDetails` sẽ forward sang `error.type`
|
|
2247
|
+
errors?: ProblemError[]; // Có khi lỗi đi qua handler; vắng mặt cho lỗi middleware/filter
|
|
2248
|
+
traceId?: string; // W3C trace ID — tự động thêm bởi framework
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// Một phần tử trong mảng `errors`
|
|
2252
|
+
interface ProblemError {
|
|
2253
|
+
code: string; // Mã định danh lỗi, ví dụ: "Bot.NotFound", "UniqueName"
|
|
2254
|
+
description: string; // Mô tả chi tiết lỗi
|
|
2255
|
+
type: string; // ErrorType: "Validation", "NotFound", "Conflict", "Forbidden", "Unauthorized", "Unexpected"
|
|
1982
2256
|
}
|
|
1983
2257
|
```
|
|
1984
2258
|
|
|
@@ -1989,7 +2263,7 @@ interface ProblemDetails {
|
|
|
1989
2263
|
Power users có thể khởi tạo `ChatHubClient` hoặc `NotificationHubClient` trực tiếp, không qua `ChatClient` facade:
|
|
1990
2264
|
|
|
1991
2265
|
```ts
|
|
1992
|
-
import { ChatHubClient, NotificationHubClient } from 'chat-client-sdk';
|
|
2266
|
+
import { ChatHubClient, NotificationHubClient } from '@manonero/chat-client-sdk';
|
|
1993
2267
|
import { LogLevel } from '@microsoft/signalr';
|
|
1994
2268
|
|
|
1995
2269
|
// Tự quản lý token
|
|
@@ -2012,7 +2286,7 @@ await chatHub.connect();
|
|
|
2012
2286
|
await notificationHub.connect();
|
|
2013
2287
|
|
|
2014
2288
|
// Kết hợp với ReconnectionManager
|
|
2015
|
-
import { ReconnectionManager } from 'chat-client-sdk';
|
|
2289
|
+
import { ReconnectionManager } from '@manonero/chat-client-sdk';
|
|
2016
2290
|
|
|
2017
2291
|
const manager = new ReconnectionManager({
|
|
2018
2292
|
chatHub,
|
|
@@ -2076,33 +2350,63 @@ const cursor = Number(result.nextCursor); // BUG!
|
|
|
2076
2350
|
|
|
2077
2351
|
- `setToken()` hoạt động ngay cho **HTTP requests**.
|
|
2078
2352
|
- SignalR đọc token khi **connect()**, không phải mid-session.
|
|
2079
|
-
- Để
|
|
2353
|
+
- Để áp token mới ngay cho hub đang Connected: `setToken(newToken)` → `await refreshConnections()`.
|
|
2354
|
+
- Sau `refreshConnections()` cần tự `joinConversation()` / `subscribeToPresence()` lại (tracking bị clear khi disconnect). Hoặc dùng `ReconnectionManager` để tự động khôi phục.
|
|
2355
|
+
- Khi `tokenProvider` được cấu hình, provider luôn được hỏi trước. `setToken()` chỉ là fallback cho trường hợp provider trả `null`/empty.
|
|
2080
2356
|
|
|
2081
2357
|
### REST vs SignalR — field name khác nhau
|
|
2082
2358
|
|
|
2083
2359
|
| Thao tác | REST | SignalR |
|
|
2084
2360
|
|----------|------|---------|
|
|
2085
|
-
| Edit message | `{ blocks, mentions }` | `{ messageId, newBlocks, newMentions }` |
|
|
2361
|
+
| Edit message | `{ blocks, mentions, replyToMessageId? }` | `{ messageId, newBlocks, newMentions, newReplyToMessageId? }` |
|
|
2086
2362
|
| Delete message | `messages.delete(messageId)` | `deleteMessage({ messageId })` — object, không phải string |
|
|
2087
2363
|
| Stream completion | Không có event | `streamCompleted` thay `messageReceived` |
|
|
2088
2364
|
|
|
2089
2365
|
### File upload — storageKey vs URL
|
|
2090
2366
|
|
|
2367
|
+
**Khi gửi tin nhắn (client → server):** dùng `storageKey` trong block, không dùng `url`.
|
|
2368
|
+
|
|
2091
2369
|
```ts
|
|
2092
|
-
// Sau khi upload, dùng storageKey làm MediaReference
|
|
2093
2370
|
const { storageKey } = await client.files.uploadFile(file);
|
|
2094
2371
|
|
|
2095
|
-
// Trong block:
|
|
2096
2372
|
const block: ImageBlock = {
|
|
2097
2373
|
$type: 'image',
|
|
2098
|
-
source: { storageKey }, // KHÔNG
|
|
2374
|
+
source: { storageKey }, // ĐÚNG — KHÔNG đặt url ở đây
|
|
2099
2375
|
// ...
|
|
2100
2376
|
};
|
|
2377
|
+
```
|
|
2378
|
+
|
|
2379
|
+
**Khi hiển thị tin nhắn nhận từ server:** server trả về `MediaReference` có thể chứa cả `storageKey` lẫn `url`. Dùng helper resolve sau:
|
|
2380
|
+
|
|
2381
|
+
```ts
|
|
2382
|
+
// resolve MediaReference → URL tuyệt đối để dùng trong <img src>, <a href>, v.v.
|
|
2383
|
+
function resolveMediaRef(
|
|
2384
|
+
ref: MediaReference | null | undefined,
|
|
2385
|
+
baseUrl: string,
|
|
2386
|
+
getDownloadUrl: (key: string) => string,
|
|
2387
|
+
): string | null {
|
|
2388
|
+
if (!ref) return null;
|
|
2389
|
+
if (ref.url) {
|
|
2390
|
+
// url có thể là relative signed URL ("/api/files/...?sig=...")
|
|
2391
|
+
// hoặc absolute external URL ("https://...")
|
|
2392
|
+
return ref.url.startsWith('/') ? baseUrl.replace(/\/$/, '') + ref.url : ref.url;
|
|
2393
|
+
}
|
|
2394
|
+
if (ref.storageKey) return getDownloadUrl(ref.storageKey);
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2101
2397
|
|
|
2102
|
-
//
|
|
2103
|
-
const displayUrl =
|
|
2398
|
+
// Ví dụ dùng với ImageBlock:
|
|
2399
|
+
const displayUrl = resolveMediaRef(
|
|
2400
|
+
block.source,
|
|
2401
|
+
'https://chat-api.example.com',
|
|
2402
|
+
client.files.getDownloadUrl.bind(client.files),
|
|
2403
|
+
);
|
|
2104
2404
|
```
|
|
2105
2405
|
|
|
2406
|
+
> **Tại sao không chỉ dùng `getDownloadUrl(storageKey)`?** Khi server bật signed download, `getDownloadUrl(storageKey)` tạo unsigned URL → server từ chối (`400 Invalid Signature`). `url` trong `MediaReference` đã được server gắn sẵn signature và phải được ưu tiên dùng.
|
|
2407
|
+
|
|
2408
|
+
> **Tại sao không dùng `url` trực tiếp?** Server trả `url` dạng relative (`/api/files/...`), không phải absolute. Dùng trực tiếp trong `<img src>` sẽ resolve về origin của frontend thay vì API server nếu hai service chạy trên host/port khác nhau.
|
|
2409
|
+
|
|
2106
2410
|
### BotDto — không có tên/avatar
|
|
2107
2411
|
|
|
2108
2412
|
`BotDto` **không** chứa `uniqueName` hay `fullName`. Phải gọi thêm:
|
|
@@ -2131,8 +2435,8 @@ const ms = Number(health.totalDuration); // NaN
|
|
|
2131
2435
|
### Ví dụ 1: Chat app cơ bản
|
|
2132
2436
|
|
|
2133
2437
|
```ts
|
|
2134
|
-
import { ChatClient, ChatApiError } from 'chat-client-sdk';
|
|
2135
|
-
import type { ChatMessageDto } from 'chat-client-sdk';
|
|
2438
|
+
import { ChatClient, ChatApiError } from '@manonero/chat-client-sdk';
|
|
2439
|
+
import type { ChatMessageDto } from '@manonero/chat-client-sdk';
|
|
2136
2440
|
|
|
2137
2441
|
// 1. Khởi tạo
|
|
2138
2442
|
const client = new ChatClient({ baseUrl: 'https://chat-api.example.com' });
|
|
@@ -2302,7 +2606,7 @@ client.realtime.chat.on('streamAborted', (dto) => {
|
|
|
2302
2606
|
### Ví dụ 5: Xử lý lỗi và token refresh
|
|
2303
2607
|
|
|
2304
2608
|
```ts
|
|
2305
|
-
import { ReconnectionManager } from 'chat-client-sdk';
|
|
2609
|
+
import { ReconnectionManager } from '@manonero/chat-client-sdk';
|
|
2306
2610
|
|
|
2307
2611
|
let isRefreshing = false;
|
|
2308
2612
|
|