@manonero/chat-client-sdk 1.0.0-beta.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 +2343 -0
- package/dist/ChatClient.d.ts +122 -0
- package/dist/ChatClient.d.ts.map +1 -0
- package/dist/ChatClient.js +173 -0
- package/dist/ChatClient.js.map +1 -0
- package/dist/errors/ChatApiError.d.ts +22 -0
- package/dist/errors/ChatApiError.d.ts.map +1 -0
- package/dist/errors/ChatApiError.js +50 -0
- package/dist/errors/ChatApiError.js.map +1 -0
- package/dist/http/AuthApi.d.ts +26 -0
- package/dist/http/AuthApi.d.ts.map +1 -0
- package/dist/http/AuthApi.js +35 -0
- package/dist/http/AuthApi.js.map +1 -0
- package/dist/http/BotApi.d.ts +50 -0
- package/dist/http/BotApi.d.ts.map +1 -0
- package/dist/http/BotApi.js +67 -0
- package/dist/http/BotApi.js.map +1 -0
- package/dist/http/ConversationApi.d.ts +81 -0
- package/dist/http/ConversationApi.d.ts.map +1 -0
- package/dist/http/ConversationApi.js +115 -0
- package/dist/http/ConversationApi.js.map +1 -0
- package/dist/http/FileApi.d.ts +48 -0
- package/dist/http/FileApi.d.ts.map +1 -0
- package/dist/http/FileApi.js +68 -0
- package/dist/http/FileApi.js.map +1 -0
- package/dist/http/HealthApi.d.ts +26 -0
- package/dist/http/HealthApi.d.ts.map +1 -0
- package/dist/http/HealthApi.js +31 -0
- package/dist/http/HealthApi.js.map +1 -0
- package/dist/http/HttpClient.d.ts +51 -0
- package/dist/http/HttpClient.d.ts.map +1 -0
- package/dist/http/HttpClient.js +182 -0
- package/dist/http/HttpClient.js.map +1 -0
- package/dist/http/MessageApi.d.ts +67 -0
- package/dist/http/MessageApi.d.ts.map +1 -0
- package/dist/http/MessageApi.js +91 -0
- package/dist/http/MessageApi.js.map +1 -0
- package/dist/http/ParticipantApi.d.ts +28 -0
- package/dist/http/ParticipantApi.d.ts.map +1 -0
- package/dist/http/ParticipantApi.js +37 -0
- package/dist/http/ParticipantApi.js.map +1 -0
- package/dist/http/ProxyApi.d.ts +41 -0
- package/dist/http/ProxyApi.d.ts.map +1 -0
- package/dist/http/ProxyApi.js +57 -0
- package/dist/http/ProxyApi.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/realtime/ChatHubClient.d.ts +87 -0
- package/dist/realtime/ChatHubClient.d.ts.map +1 -0
- package/dist/realtime/ChatHubClient.js +182 -0
- package/dist/realtime/ChatHubClient.js.map +1 -0
- package/dist/realtime/NotificationHubClient.d.ts +73 -0
- package/dist/realtime/NotificationHubClient.d.ts.map +1 -0
- package/dist/realtime/NotificationHubClient.js +162 -0
- package/dist/realtime/NotificationHubClient.js.map +1 -0
- package/dist/realtime/ReconnectionManager.d.ts +65 -0
- package/dist/realtime/ReconnectionManager.d.ts.map +1 -0
- package/dist/realtime/ReconnectionManager.js +118 -0
- package/dist/realtime/ReconnectionManager.js.map +1 -0
- package/dist/types/auth.d.ts +28 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/block.d.ts +148 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +47 -0
- package/dist/types/block.js.map +1 -0
- package/dist/types/bot.d.ts +48 -0
- package/dist/types/bot.d.ts.map +1 -0
- package/dist/types/bot.js +3 -0
- package/dist/types/bot.js.map +1 -0
- package/dist/types/chat-events.d.ts +153 -0
- package/dist/types/chat-events.d.ts.map +1 -0
- package/dist/types/chat-events.js +3 -0
- package/dist/types/chat-events.js.map +1 -0
- package/dist/types/common.d.ts +57 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +3 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/conversation.d.ts +70 -0
- package/dist/types/conversation.d.ts.map +1 -0
- package/dist/types/conversation.js +3 -0
- package/dist/types/conversation.js.map +1 -0
- package/dist/types/file.d.ts +31 -0
- package/dist/types/file.d.ts.map +1 -0
- package/dist/types/file.js +3 -0
- package/dist/types/file.js.map +1 -0
- package/dist/types/message.d.ts +83 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/notification-events.d.ts +97 -0
- package/dist/types/notification-events.d.ts.map +1 -0
- package/dist/types/notification-events.js +3 -0
- package/dist/types/notification-events.js.map +1 -0
- package/dist/types/participant.d.ts +25 -0
- package/dist/types/participant.d.ts.map +1 -0
- package/dist/types/participant.js +3 -0
- package/dist/types/participant.js.map +1 -0
- package/dist/types/signalr.d.ts +87 -0
- package/dist/types/signalr.d.ts.map +1 -0
- package/dist/types/signalr.js +3 -0
- package/dist/types/signalr.js.map +1 -0
- package/dist/utils/TypedEventEmitter.d.ts +32 -0
- package/dist/utils/TypedEventEmitter.d.ts.map +1 -0
- package/dist/utils/TypedEventEmitter.js +60 -0
- package/dist/utils/TypedEventEmitter.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,2343 @@
|
|
|
1
|
+
# ChatClientSdk — Tài liệu hướng dẫn sử dụng
|
|
2
|
+
|
|
3
|
+
> SDK TypeScript để tích hợp với Chat Gateway API. Bao gồm toàn bộ HTTP REST API và kết nối real-time qua SignalR.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Mục lục
|
|
8
|
+
|
|
9
|
+
1. [Tổng quan kiến trúc](#1-tổng-quan-kiến-trúc)
|
|
10
|
+
2. [Cài đặt và khởi tạo](#2-cài-đặt-và-khởi-tạo)
|
|
11
|
+
3. [ChatClient — Facade chính](#3-chatclient--facade-chính)
|
|
12
|
+
4. [Xác thực — AuthApi](#4-xác-thực--authapi)
|
|
13
|
+
5. [Participant — ParticipantApi](#5-participant--participantapi)
|
|
14
|
+
6. [Conversation — ConversationApi](#6-conversation--conversationapi)
|
|
15
|
+
7. [Message — MessageApi](#7-message--messageapi)
|
|
16
|
+
8. [File — FileApi](#8-file--fileapi)
|
|
17
|
+
9. [Bot — BotApi](#9-bot--botapi)
|
|
18
|
+
10. [Proxy — ProxyApi](#10-proxy--proxyapi)
|
|
19
|
+
11. [Health — HealthApi](#11-health--healthapi)
|
|
20
|
+
12. [Real-time: ChatHubClient](#12-real-time-chathubclient)
|
|
21
|
+
13. [Real-time: NotificationHubClient](#13-real-time-notificationhubclient)
|
|
22
|
+
14. [ReconnectionManager](#14-reconnectionmanager)
|
|
23
|
+
15. [Hệ thống Block (nội dung tin nhắn)](#15-hệ-thống-block-nội-dung-tin-nhắn)
|
|
24
|
+
16. [Toàn bộ kiểu dữ liệu (Types)](#16-toàn-bộ-kiểu-dữ-liệu-types)
|
|
25
|
+
17. [Xử lý lỗi — ChatApiError](#17-xử-lý-lỗi--chatapierror)
|
|
26
|
+
18. [TypedEventEmitter](#18-typedeventemitter)
|
|
27
|
+
19. [Lưu ý quan trọng](#19-lưu-ý-quan-trọng)
|
|
28
|
+
20. [Ví dụ đầy đủ](#20-ví-dụ-đầy-đủ)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Tổng quan kiến trúc
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ChatClient (facade chính)
|
|
36
|
+
├── auth → AuthApi (xác thực)
|
|
37
|
+
├── participants → ParticipantApi (quản lý người dùng)
|
|
38
|
+
├── conversations → ConversationApi (quản lý cuộc hội thoại)
|
|
39
|
+
├── messages → MessageApi (tin nhắn)
|
|
40
|
+
├── files → FileApi (upload/download file)
|
|
41
|
+
├── bots → BotApi (quản lý bot)
|
|
42
|
+
├── proxy → ProxyApi (proxy đến backend khác)
|
|
43
|
+
├── health → HealthApi (kiểm tra sức khỏe server)
|
|
44
|
+
└── realtime
|
|
45
|
+
├── chat → ChatHubClient (sự kiện tin nhắn real-time)
|
|
46
|
+
└── notifications → NotificationHubClient (thông báo real-time)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Luồng hoạt động cơ bản:**
|
|
50
|
+
|
|
51
|
+
1. Khởi tạo `ChatClient` với `baseUrl`.
|
|
52
|
+
2. Đăng nhập qua `client.auth.loginWith*()` — token được tự động lưu và truyền vào mọi request sau.
|
|
53
|
+
3. Kết nối các hub SignalR qua `client.realtime.notifications.connect()` và `client.realtime.chat.connect()`.
|
|
54
|
+
4. Lắng nghe sự kiện real-time bằng `.on(eventName, handler)`.
|
|
55
|
+
5. Gọi các phương thức REST hoặc Hub để thực hiện thao tác.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Cài đặt và khởi tạo
|
|
60
|
+
|
|
61
|
+
### Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install chat-client-sdk
|
|
65
|
+
npm install @microsoft/signalr # peer dependency
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Import
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { ChatClient } from 'chat-client-sdk';
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Tất cả các kiểu dữ liệu đều được export từ package root:
|
|
75
|
+
|
|
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
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Export nâng cao** — SDK cũng export một số class và type nội bộ cho power users:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
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';
|
|
87
|
+
|
|
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';
|
|
91
|
+
|
|
92
|
+
// Event map types — dùng khi cần typed wrapper
|
|
93
|
+
import type { ChatHubEventMap, NotificationHubEventMap } from 'chat-client-sdk';
|
|
94
|
+
|
|
95
|
+
// Hub options (khi dùng hub client độc lập, không qua ChatClient)
|
|
96
|
+
import type { ChatHubClientOptions, NotificationHubClientOptions } from 'chat-client-sdk';
|
|
97
|
+
|
|
98
|
+
// Hub request types (Client → Server) — dùng khi gọi SignalR methods
|
|
99
|
+
import type {
|
|
100
|
+
ChatSendMessageRequest,
|
|
101
|
+
ChatEditMessageRequest,
|
|
102
|
+
ChatDeleteMessageRequest,
|
|
103
|
+
ChatRecoverMessageRequest,
|
|
104
|
+
ChatAddReactionRequest,
|
|
105
|
+
ChatRemoveReactionRequest,
|
|
106
|
+
} from 'chat-client-sdk';
|
|
107
|
+
|
|
108
|
+
// Ack types (Server → Client return) — dùng khi cần type return value của Hub methods
|
|
109
|
+
import type {
|
|
110
|
+
SendMessageAck,
|
|
111
|
+
EditMessageAck,
|
|
112
|
+
DeleteMessageAck,
|
|
113
|
+
RecoverMessageAck,
|
|
114
|
+
ReactionAck,
|
|
115
|
+
MarkAsReadAck,
|
|
116
|
+
HubErrorDto,
|
|
117
|
+
} from 'chat-client-sdk';
|
|
118
|
+
|
|
119
|
+
// ReconnectionManager options
|
|
120
|
+
import type { ReconnectionManagerOptions } from 'chat-client-sdk';
|
|
121
|
+
|
|
122
|
+
// ChatClient facade types
|
|
123
|
+
import type { ChatClientSignalrOptions, ChatClientRealtime } from 'chat-client-sdk';
|
|
124
|
+
|
|
125
|
+
// Upload progress callback type — dùng khi cần type hàm onProgress
|
|
126
|
+
import type { UploadProgressCallback } from 'chat-client-sdk';
|
|
127
|
+
|
|
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';
|
|
130
|
+
|
|
131
|
+
// Mark as read request type (REST)
|
|
132
|
+
import type { MarkAsReadRequest } from 'chat-client-sdk';
|
|
133
|
+
|
|
134
|
+
// Login request types riêng lẻ (khi cần type-check từng provider)
|
|
135
|
+
import type {
|
|
136
|
+
GoogleLoginRequest,
|
|
137
|
+
DsAccountLoginRequest,
|
|
138
|
+
DevLoginRequest,
|
|
139
|
+
LoginRequest,
|
|
140
|
+
} from 'chat-client-sdk';
|
|
141
|
+
|
|
142
|
+
// Standalone hub clients — dùng khi không cần ChatClient facade
|
|
143
|
+
import { ChatHubClient, NotificationHubClient, ReconnectionManager } from 'chat-client-sdk';
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Khởi tạo cơ bản
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const client = new ChatClient({
|
|
150
|
+
baseUrl: 'https://chat-api.example.com',
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Khởi tạo với token có sẵn (đã đăng nhập từ trước)
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const client = new ChatClient({
|
|
158
|
+
baseUrl: 'https://chat-api.example.com',
|
|
159
|
+
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Khởi tạo với external token provider
|
|
164
|
+
|
|
165
|
+
Dùng khi token được quản lý bên ngoài SDK (ví dụ: Redux store):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const client = new ChatClient({
|
|
169
|
+
baseUrl: 'https://chat-api.example.com',
|
|
170
|
+
tokenProvider: () => store.getState().auth.token,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> **Ưu tiên token:** `token` (hoặc `setToken()`) luôn ưu tiên hơn `tokenProvider`. Khi `_token` khác `null`, `tokenProvider` bị bỏ qua.
|
|
175
|
+
|
|
176
|
+
### Cấu hình SignalR log level
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { LogLevel } from '@microsoft/signalr';
|
|
180
|
+
|
|
181
|
+
const client = new ChatClient({
|
|
182
|
+
baseUrl: 'https://chat-api.example.com',
|
|
183
|
+
signalrOptions: {
|
|
184
|
+
logLevel: LogLevel.Debug, // Mặc định: LogLevel.Warning
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 3. ChatClient — Facade chính
|
|
192
|
+
|
|
193
|
+
**Class:** `ChatClient`
|
|
194
|
+
|
|
195
|
+
Đây là điểm vào duy nhất của SDK. Quản lý token JWT dùng chung cho tất cả HTTP request và kết nối SignalR.
|
|
196
|
+
|
|
197
|
+
### Constructor
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
new ChatClient(options: ChatClientOptions)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| Option | Kiểu | Bắt buộc | Mô tả |
|
|
204
|
+
|--------|------|----------|-------|
|
|
205
|
+
| `baseUrl` | `string` | ✅ | URL gốc của server, ví dụ `"https://chat-api.example.com"` |
|
|
206
|
+
| `token` | `string` | ❌ | JWT token ban đầu (nếu đã đăng nhập) |
|
|
207
|
+
| `tokenProvider` | `() => string \| null` | ❌ | Hàm lấy token từ bên ngoài |
|
|
208
|
+
| `signalrOptions.logLevel` | `LogLevel` | ❌ | Mức log cho SignalR (mặc định: `Warning`) |
|
|
209
|
+
|
|
210
|
+
### Thuộc tính
|
|
211
|
+
|
|
212
|
+
| Thuộc tính | Kiểu | Mô tả |
|
|
213
|
+
|------------|------|-------|
|
|
214
|
+
| `auth` | `AuthApi` | Các endpoint xác thực |
|
|
215
|
+
| `participants` | `ParticipantApi` | Quản lý participant |
|
|
216
|
+
| `conversations` | `ConversationApi` | Quản lý cuộc hội thoại |
|
|
217
|
+
| `messages` | `MessageApi` | Thao tác với tin nhắn |
|
|
218
|
+
| `files` | `FileApi` | Upload/download file |
|
|
219
|
+
| `bots` | `BotApi` | Quản lý bot |
|
|
220
|
+
| `proxy` | `ProxyApi` | Proxy đến backend khác |
|
|
221
|
+
| `health` | `HealthApi` | Kiểm tra sức khỏe server |
|
|
222
|
+
| `realtime.chat` | `ChatHubClient` | SignalR ChatHub |
|
|
223
|
+
| `realtime.notifications` | `NotificationHubClient` | SignalR NotificationHub |
|
|
224
|
+
| `currentUser` | `AuthUserInfo \| null` | User hiện tại sau khi đăng nhập |
|
|
225
|
+
|
|
226
|
+
### Phương thức
|
|
227
|
+
|
|
228
|
+
#### `setToken(token: string): void`
|
|
229
|
+
|
|
230
|
+
Cập nhật JWT token. Có hiệu lực ngay với mọi HTTP request tiếp theo.
|
|
231
|
+
|
|
232
|
+
> ⚠️ Các kết nối SignalR đang hoạt động **không** được cập nhật token mid-session. Cần `disconnect()` rồi `connect()` lại để dùng token mới.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
client.setToken('new-jwt-token-here');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### `disconnect(): Promise<void>`
|
|
239
|
+
|
|
240
|
+
Ngắt kết nối tất cả các SignalR hub (ChatHub + NotificationHub) đồng thời.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
await client.disconnect();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 4. Xác thực — AuthApi
|
|
249
|
+
|
|
250
|
+
**Truy cập:** `client.auth`
|
|
251
|
+
|
|
252
|
+
Các endpoint xác thực **không yêu cầu JWT**. Sau khi đăng nhập thành công, SDK tự động lưu token và cập nhật `client.currentUser`.
|
|
253
|
+
|
|
254
|
+
### `loginWithGoogle(idToken: string): Promise<AuthResponse>`
|
|
255
|
+
|
|
256
|
+
Đăng nhập bằng Google ID Token.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const { token, user } = await client.auth.loginWithGoogle(googleIdToken);
|
|
260
|
+
console.log(client.currentUser); // AuthUserInfo đã được set tự động
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### `loginWithDsAccount(token: string): Promise<AuthResponse>`
|
|
264
|
+
|
|
265
|
+
Đăng nhập bằng DS Account token.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const auth = await client.auth.loginWithDsAccount(dsToken);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `loginWithDev(username: string, password: string): Promise<AuthResponse>`
|
|
272
|
+
|
|
273
|
+
Đăng nhập tài khoản dev. Chỉ dùng trong môi trường phát triển. JWT được cấp có claim `role=dev`, cho phép truy cập các endpoint quản lý bot.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const auth = await client.auth.loginWithDev('devuser', 'devpass');
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `login(provider: string, body: LoginRequest): Promise<AuthResponse>`
|
|
280
|
+
|
|
281
|
+
Đăng nhập với provider tùy chỉnh (khi provider được xác định lúc runtime).
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const auth = await client.auth.login('custom-provider', { token: '...' });
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `AuthResponse`
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
interface AuthResponse {
|
|
291
|
+
token: string; // JWT token
|
|
292
|
+
expiresAt: string; // ISO 8601 — thời điểm hết hạn
|
|
293
|
+
user: AuthUserInfo;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface AuthUserInfo {
|
|
297
|
+
id: string;
|
|
298
|
+
uniqueName: string;
|
|
299
|
+
fullName: string;
|
|
300
|
+
avatar?: string; // URL thuần (KHÔNG phải MediaReference)
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 5. Participant — ParticipantApi
|
|
307
|
+
|
|
308
|
+
**Truy cập:** `client.participants`
|
|
309
|
+
|
|
310
|
+
Tất cả endpoint đều **yêu cầu JWT**. Server cache GET trong 5 phút.
|
|
311
|
+
|
|
312
|
+
### `createOrUpdate(request): Promise<ParticipantDto>`
|
|
313
|
+
|
|
314
|
+
Tạo mới hoặc cập nhật participant (upsert theo ID).
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const participant = await client.participants.createOrUpdate({
|
|
318
|
+
id: 'user-123',
|
|
319
|
+
uniqueName: 'john_doe',
|
|
320
|
+
fullName: 'John Doe',
|
|
321
|
+
avatar: { url: 'https://example.com/avatar.jpg' },
|
|
322
|
+
gender: 'Male',
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### `getById(id: string): Promise<ParticipantDto>`
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const participant = await client.participants.getById('user-123');
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### `getByUniqueName(uniqueName: string): Promise<ParticipantDto>`
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const participant = await client.participants.getByUniqueName('john_doe');
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### `getBatch(ids: string[]): Promise<ParticipantDto[]>`
|
|
339
|
+
|
|
340
|
+
Lấy nhiều participant trong một request. ID không tìm thấy sẽ bị bỏ qua (không báo lỗi).
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const participants = await client.participants.getBatch(['id-1', 'id-2', 'id-3']);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `ParticipantDto`
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
interface ParticipantDto {
|
|
350
|
+
id: string;
|
|
351
|
+
uniqueName: string;
|
|
352
|
+
fullName: string;
|
|
353
|
+
avatar?: MediaReference;
|
|
354
|
+
gender?: string;
|
|
355
|
+
isBot: boolean;
|
|
356
|
+
isOnline: boolean;
|
|
357
|
+
lastSeenAt?: string; // ISO 8601
|
|
358
|
+
createdAt: string; // ISO 8601
|
|
359
|
+
updatedAt: string; // ISO 8601
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 6. Conversation — ConversationApi
|
|
366
|
+
|
|
367
|
+
**Truy cập:** `client.conversations`
|
|
368
|
+
|
|
369
|
+
Tất cả endpoint đều **yêu cầu JWT**.
|
|
370
|
+
|
|
371
|
+
### `list(params?): Promise<PaginatedResult<ConversationListItemDto>>`
|
|
372
|
+
|
|
373
|
+
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
|
+
|
|
375
|
+
```ts
|
|
376
|
+
// Trang đầu
|
|
377
|
+
const result = await client.conversations.list({ limit: 20 });
|
|
378
|
+
console.log(result.items); // ConversationListItemDto[]
|
|
379
|
+
console.log(result.hasMore); // boolean
|
|
380
|
+
console.log(result.nextCursor); // string | null
|
|
381
|
+
|
|
382
|
+
// Trang tiếp theo
|
|
383
|
+
if (result.hasMore) {
|
|
384
|
+
const nextPage = await client.conversations.list({
|
|
385
|
+
limit: 20,
|
|
386
|
+
cursor: result.nextCursor!, // Đây là string — KHÔNG cast sang number
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
> ⚠️ `cursor` là chuỗi .NET DateTime ticks (ví dụ: `"638836680000000000"`, 18 chữ số). Vượt quá `Number.MAX_SAFE_INTEGER` — **KHÔNG được ép kiểu sang number**.
|
|
392
|
+
|
|
393
|
+
### `create(request): Promise<ConversationDto>`
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// Tạo cuộc hội thoại nhóm
|
|
397
|
+
const conv = await client.conversations.create({
|
|
398
|
+
type: 'Group',
|
|
399
|
+
name: 'Team Chat',
|
|
400
|
+
avatar: { storageKey: '01HXABCDEF/group-avatar.jpg' }, // optional
|
|
401
|
+
participantIds: ['user-1', 'user-2', 'user-3'],
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Tạo cuộc hội thoại 1-1
|
|
405
|
+
const dm = await client.conversations.create({
|
|
406
|
+
type: 'OneToOne',
|
|
407
|
+
participantIds: ['other-user-id'],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Tạo cuộc hội thoại với chính mình (Saved Messages)
|
|
411
|
+
const self = await client.conversations.create({
|
|
412
|
+
type: 'Self',
|
|
413
|
+
participantIds: [],
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### `search(query, limit?): Promise<ConversationListItemDto[]>`
|
|
418
|
+
|
|
419
|
+
Tìm kiếm theo tên cuộc hội thoại hoặc tên participant.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const results = await client.conversations.search('team', 10);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `getById(id: string): Promise<ConversationDto>`
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
const conv = await client.conversations.getById('conv-id-here');
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### `update(id, request): Promise<ConversationDto>`
|
|
432
|
+
|
|
433
|
+
Chỉ áp dụng cho cuộc hội thoại **Group**.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
const updated = await client.conversations.update('conv-id', {
|
|
437
|
+
name: 'New Group Name',
|
|
438
|
+
avatar: { url: 'https://example.com/group-avatar.jpg' },
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### `leave(id: string): Promise<void>`
|
|
443
|
+
|
|
444
|
+
Rời khỏi cuộc hội thoại. Các participant khác vẫn còn đó.
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
await client.conversations.leave('conv-id');
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### `findOneToOne(otherParticipantId): Promise<ConversationDto | null>`
|
|
451
|
+
|
|
452
|
+
Tìm cuộc hội thoại 1-1 với user khác. Trả về `null` (HTTP 204) nếu chưa có.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
const existing = await client.conversations.findOneToOne('other-user-id');
|
|
456
|
+
if (!existing) {
|
|
457
|
+
// Tạo mới
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### `addParticipant(conversationId, request): Promise<void>`
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
await client.conversations.addParticipant('conv-id', {
|
|
465
|
+
participantId: 'new-user-id',
|
|
466
|
+
canViewHistory: true, // Mặc định true — xem được tin nhắn cũ
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `removeParticipant(conversationId, participantId): Promise<void>`
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
await client.conversations.removeParticipant('conv-id', 'user-to-remove-id');
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `getParticipants(conversationId): Promise<ConversationParticipantDto[]>`
|
|
477
|
+
|
|
478
|
+
Trả về tất cả participant kể cả người đã rời nhóm.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
const members = await client.conversations.getParticipants('conv-id');
|
|
482
|
+
const active = members.filter(m => m.isActive);
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### `pin(id) / unpin(id): Promise<void>`
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
await client.conversations.pin('conv-id');
|
|
489
|
+
await client.conversations.unpin('conv-id');
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### `markAsRead(id, messageId) / markAsUnread(id): Promise<void>`
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
// Đánh dấu đã đọc đến messageId
|
|
496
|
+
await client.conversations.markAsRead('conv-id', 'last-message-id');
|
|
497
|
+
|
|
498
|
+
// Đánh dấu chưa đọc
|
|
499
|
+
await client.conversations.markAsUnread('conv-id');
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Kiểu dữ liệu Conversation
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
type ConversationType = 'Group' | 'OneToOne' | 'Self';
|
|
506
|
+
|
|
507
|
+
interface ConversationDto {
|
|
508
|
+
id: string;
|
|
509
|
+
type: ConversationType;
|
|
510
|
+
name?: string; // Tên nhóm (chỉ Group)
|
|
511
|
+
avatar?: MediaReference;
|
|
512
|
+
ownerId: string;
|
|
513
|
+
lastMessageAt?: string; // ISO 8601
|
|
514
|
+
lastMessageId?: string;
|
|
515
|
+
createdAt: string; // ISO 8601
|
|
516
|
+
updatedAt: string; // ISO 8601
|
|
517
|
+
participants: ConversationParticipantDto[];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
interface ConversationParticipantDto {
|
|
521
|
+
participantId: string;
|
|
522
|
+
joinedAt: string; // ISO 8601
|
|
523
|
+
leftAt?: string | null; // ISO 8601, null nếu vẫn trong nhóm
|
|
524
|
+
isActive: boolean;
|
|
525
|
+
isPinned: boolean;
|
|
526
|
+
pinnedAt?: string | null; // ISO 8601
|
|
527
|
+
lastReadMessageId?: string | null;
|
|
528
|
+
lastReadAt?: string | null; // ISO 8601
|
|
529
|
+
unreadCount: number;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
interface ConversationListItemDto {
|
|
533
|
+
id: string;
|
|
534
|
+
type: ConversationType;
|
|
535
|
+
name?: string;
|
|
536
|
+
avatar?: MediaReference;
|
|
537
|
+
lastMessageAt?: string; // ISO 8601
|
|
538
|
+
lastMessageId?: string;
|
|
539
|
+
lastMessage?: MessageDto; // Preview tin nhắn cuối
|
|
540
|
+
isPinned: boolean;
|
|
541
|
+
pinnedAt?: string; // ISO 8601
|
|
542
|
+
unreadCount: number;
|
|
543
|
+
participantCount: number;
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 7. Message — MessageApi
|
|
550
|
+
|
|
551
|
+
**Truy cập:** `client.messages`
|
|
552
|
+
|
|
553
|
+
Tất cả endpoint đều **yêu cầu JWT**.
|
|
554
|
+
|
|
555
|
+
### `send(conversationId, request): Promise<MessageDto>`
|
|
556
|
+
|
|
557
|
+
Gửi tin nhắn qua REST API. Trả về 201 Created với `MessageDto` đầy đủ.
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
const msg = await client.messages.send('conv-id', {
|
|
561
|
+
blocks: [
|
|
562
|
+
{ $type: 'text', format: 'Plain', content: 'Xin chào!', plainText: 'Xin chào!' }
|
|
563
|
+
],
|
|
564
|
+
replyToMessageId: 'msg-id-to-reply', // optional
|
|
565
|
+
clientMessageId: 'local-uuid-1234', // optional, dùng cho optimistic rendering
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
> 💡 **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
|
+
|
|
571
|
+
### `getHistory(conversationId, params?): Promise<PaginatedResult<MessageDto>>`
|
|
572
|
+
|
|
573
|
+
Lấy lịch sử tin nhắn với cursor-based pagination.
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
// Lấy 30 tin nhắn mới nhất
|
|
577
|
+
const result = await client.messages.getHistory('conv-id', { limit: 30 });
|
|
578
|
+
|
|
579
|
+
// Load thêm tin nhắn cũ hơn (scroll lên)
|
|
580
|
+
const older = await client.messages.getHistory('conv-id', {
|
|
581
|
+
limit: 30,
|
|
582
|
+
cursor: result.nextCursor!,
|
|
583
|
+
direction: 'older', // mặc định
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Load tin nhắn mới hơn cursor (scroll xuống)
|
|
587
|
+
const newer = await client.messages.getHistory('conv-id', {
|
|
588
|
+
cursor: someCursor,
|
|
589
|
+
direction: 'newer',
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### `search(conversationId, query, limit?): Promise<MessageDto[]>`
|
|
594
|
+
|
|
595
|
+
Full-text search trong cuộc hội thoại.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
const results = await client.messages.search('conv-id', 'từ khóa tìm kiếm', 20);
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### `getById(messageId): Promise<MessageDto>`
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
const msg = await client.messages.getById('msg-id');
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### `edit(messageId, request): Promise<MessageDto>`
|
|
608
|
+
|
|
609
|
+
Chỉ người gửi mới có thể chỉnh sửa.
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
const edited = await client.messages.edit('msg-id', {
|
|
613
|
+
blocks: [{ $type: 'text', format: 'Plain', content: 'Nội dung đã sửa', plainText: 'Nội dung đã sửa' }],
|
|
614
|
+
mentions: [], // optional
|
|
615
|
+
});
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
> ⚠️ **Khác biệt REST vs SignalR:** REST dùng `{ blocks, mentions }`, SignalR dùng `{ messageId, newBlocks, newMentions }`.
|
|
619
|
+
|
|
620
|
+
### `delete(messageId): Promise<{ messageId, deletedAt }>`
|
|
621
|
+
|
|
622
|
+
Xóa mềm (soft delete). Trả về object (không phải 204).
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
const { messageId, deletedAt } = await client.messages.delete('msg-id');
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### `recover(messageId): Promise<void>`
|
|
629
|
+
|
|
630
|
+
Khôi phục tin nhắn đã xóa. Chỉ khả dụng trong vòng **10 phút** sau khi xóa.
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
await client.messages.recover('msg-id');
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### `addReaction(messageId, emoji) / removeReaction(messageId, emoji): Promise<void>`
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
await client.messages.addReaction('msg-id', '👍');
|
|
640
|
+
await client.messages.removeReaction('msg-id', '👍');
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
> Server trả về 409 Conflict nếu đã react với emoji đó. Emoji được tự động URL-encode.
|
|
644
|
+
|
|
645
|
+
### Kiểu dữ liệu Message
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
type SenderType = 'User' | 'Bot' | 'System';
|
|
649
|
+
|
|
650
|
+
interface MessageDto {
|
|
651
|
+
id: string; // ULID do server tạo
|
|
652
|
+
conversationId: string;
|
|
653
|
+
senderId: string;
|
|
654
|
+
senderType: SenderType;
|
|
655
|
+
timestamp: string; // ISO 8601 — thời điểm gửi
|
|
656
|
+
blocks: Block[]; // Nội dung tin nhắn
|
|
657
|
+
plainTextIndex: string | null;
|
|
658
|
+
replyToMessageId: string | null;
|
|
659
|
+
quickReplies: QuickReply[] | null;
|
|
660
|
+
isEdited: boolean;
|
|
661
|
+
editedAt: string | null; // ISO 8601
|
|
662
|
+
isDeleted: boolean;
|
|
663
|
+
deletedAt: string | null; // ISO 8601
|
|
664
|
+
reactionSummary: ReactionSummary | null;
|
|
665
|
+
mentions: Mention[] | null;
|
|
666
|
+
createdAt: string; // ISO 8601
|
|
667
|
+
updatedAt: string; // ISO 8601
|
|
668
|
+
clientMessageId: string | null;
|
|
669
|
+
systemEvent: SystemEventInfo | null;
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Kiểu dữ liệu bổ sung
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
// Reaction summary (REST API)
|
|
677
|
+
interface ReactionSummary {
|
|
678
|
+
groups: ReactionGroup[];
|
|
679
|
+
totalCount: number;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
interface ReactionGroup {
|
|
683
|
+
emoji: string;
|
|
684
|
+
count: number;
|
|
685
|
+
participantIds: string[]; // Danh sách participant đã react
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Request để gửi tin nhắn mới
|
|
689
|
+
interface SendMessageRequest {
|
|
690
|
+
blocks: Block[];
|
|
691
|
+
replyToMessageId?: string;
|
|
692
|
+
quickReplies?: QuickReply[]; // Nút bấm nhanh đính kèm tin nhắn
|
|
693
|
+
mentions?: Mention[];
|
|
694
|
+
clientMessageId?: string; // Tối đa 128 ký tự
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Request để chỉnh sửa tin nhắn qua REST
|
|
698
|
+
interface EditMessageRequest {
|
|
699
|
+
blocks: Block[];
|
|
700
|
+
mentions?: Mention[];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Request đánh dấu đã đọc (REST)
|
|
704
|
+
interface MarkAsReadRequest {
|
|
705
|
+
messageId: string;
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## 8. File — FileApi
|
|
712
|
+
|
|
713
|
+
**Truy cập:** `client.files`
|
|
714
|
+
|
|
715
|
+
Upload yêu cầu JWT. Download là **public** (không cần JWT).
|
|
716
|
+
|
|
717
|
+
### `uploadFile(file, onProgress?): Promise<UploadResponse>`
|
|
718
|
+
|
|
719
|
+
Phương thức tiện lợi — gộp `createUploadUrl` + `upload` thành một bước.
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
const fileInput = document.getElementById('file') as HTMLInputElement;
|
|
723
|
+
const file = fileInput.files![0];
|
|
724
|
+
|
|
725
|
+
const result = await client.files.uploadFile(file, (loaded, total) => {
|
|
726
|
+
const percent = Math.round((loaded / total) * 100);
|
|
727
|
+
console.log(`Upload: ${percent}%`);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
console.log(result.storageKey); // Dùng làm MediaReference.storageKey
|
|
731
|
+
console.log(result.downloadUrl); // Relative URL
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### `createUploadUrl(request): Promise<CreateUploadUrlResponse>`
|
|
735
|
+
|
|
736
|
+
Bước 1 của 2 bước upload thủ công — xin URL upload.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
const { uploadUrl, storageKey, expiresAt } = await client.files.createUploadUrl({
|
|
740
|
+
fileName: 'photo.jpg',
|
|
741
|
+
contentType: 'image/jpeg',
|
|
742
|
+
fileSize: file.size, // bytes, tối đa 50MB
|
|
743
|
+
});
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### `upload(uploadUrl, file, onProgress?): Promise<UploadResponse>`
|
|
747
|
+
|
|
748
|
+
Bước 2 — upload file vào URL đã xin.
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
const result = await client.files.upload(uploadUrl, file, (loaded, total) => {
|
|
752
|
+
console.log(`${loaded}/${total}`);
|
|
753
|
+
});
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
> Khi có `onProgress`, SDK dùng XHR thay vì fetch (vì fetch không hỗ trợ upload progress events).
|
|
757
|
+
|
|
758
|
+
### `getDownloadUrl(storageKey: string): string`
|
|
759
|
+
|
|
760
|
+
Tạo URL tải file tuyệt đối (dùng để hiển thị ảnh, tải xuống, v.v.).
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
const url = client.files.getDownloadUrl('01HXABCDEF/photo.jpg');
|
|
764
|
+
// → "https://chat-api.example.com/api/files/01HXABCDEF/photo.jpg"
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### `delete(storageKey: string): Promise<void>`
|
|
768
|
+
|
|
769
|
+
Xóa file. Chỉ người upload mới có thể xóa. `storageKey` có thể chứa `/`, không cần encode.
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
await client.files.delete('01HXABCDEF/photo.jpg');
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Kiểu dữ liệu File
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
interface CreateUploadUrlRequest {
|
|
779
|
+
fileName: string;
|
|
780
|
+
contentType: string;
|
|
781
|
+
fileSize: number; // bytes
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
interface CreateUploadUrlResponse {
|
|
785
|
+
uploadUrl: string; // Relative path — HttpClient tự prepend baseUrl
|
|
786
|
+
storageKey: string;
|
|
787
|
+
expiresAt: string; // ISO 8601
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
interface UploadResponse {
|
|
791
|
+
downloadUrl: string; // Relative path
|
|
792
|
+
storageKey: string;
|
|
793
|
+
fileName: string;
|
|
794
|
+
contentType: string;
|
|
795
|
+
fileSize: number;
|
|
796
|
+
uploadedAt: string; // ISO 8601
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## 9. Bot — BotApi
|
|
803
|
+
|
|
804
|
+
**Truy cập:** `client.bots`
|
|
805
|
+
|
|
806
|
+
GET (`list`, `getById`) là **public**. Các thao tác quản lý yêu cầu JWT với `role=dev` (đăng nhập qua `loginWithDev`). User không có quyền nhận `404 Not Found`.
|
|
807
|
+
|
|
808
|
+
### `list(params?): Promise<PagedResult<BotDto>>`
|
|
809
|
+
|
|
810
|
+
> Lưu ý: BotApi dùng **page-based** pagination (không phải cursor-based). `page` bắt đầu từ 1. Server cache kết quả **5 phút**.
|
|
811
|
+
|
|
812
|
+
```ts
|
|
813
|
+
const result = await client.bots.list({ page: 1, pageSize: 20 });
|
|
814
|
+
console.log(result.items); // BotDto[]
|
|
815
|
+
console.log(result.totalCount); // Tổng số bot
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### `getById(id): Promise<BotDto>`
|
|
819
|
+
|
|
820
|
+
> Server cache kết quả **5 phút**.
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
const bot = await client.bots.getById('bot-id');
|
|
824
|
+
// Lấy thêm thông tin display: name, avatar
|
|
825
|
+
const participant = await client.participants.getById(bot.participantId);
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### `create(request): Promise<BotDto & { apiKey: string }>`
|
|
829
|
+
|
|
830
|
+
Tạo bot và nhận `apiKey` một lần — **lưu ngay**, server không lưu key này.
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
const bot = await client.bots.create({
|
|
834
|
+
uniqueName: 'my-bot',
|
|
835
|
+
fullName: 'My Assistant Bot',
|
|
836
|
+
description: 'Trợ lý AI',
|
|
837
|
+
kafkaTopic: 'my-bot-topic', // Không thể thay đổi sau khi tạo
|
|
838
|
+
rateLimitPerSecond: 5,
|
|
839
|
+
listenAllGroupMessages: false,
|
|
840
|
+
});
|
|
841
|
+
const apiKey = bot.apiKey; // Lưu ngay!
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### `update(id, request): Promise<BotDto>`
|
|
845
|
+
|
|
846
|
+
```ts
|
|
847
|
+
await client.bots.update('bot-id', {
|
|
848
|
+
fullName: 'Updated Bot Name',
|
|
849
|
+
rateLimitPerSecond: 10,
|
|
850
|
+
// kafkaTopic KHÔNG thể cập nhật
|
|
851
|
+
});
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### `activate(id) / deactivate(id): Promise<void>`
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
await client.bots.activate('bot-id');
|
|
858
|
+
await client.bots.deactivate('bot-id');
|
|
859
|
+
```
|
|
860
|
+
|
|
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
|
+
### `delete(id): Promise<void>`
|
|
870
|
+
|
|
871
|
+
Soft delete.
|
|
872
|
+
|
|
873
|
+
```ts
|
|
874
|
+
await client.bots.delete('bot-id');
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Kiểu dữ liệu Bot
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
interface BotDto {
|
|
881
|
+
id: string;
|
|
882
|
+
participantId: string; // Dùng để lấy name/avatar từ ParticipantApi
|
|
883
|
+
description?: string;
|
|
884
|
+
metadata?: Record<string, unknown>;
|
|
885
|
+
kafkaTopic: string; // KHÔNG thể thay đổi sau khi tạo
|
|
886
|
+
isActive: boolean;
|
|
887
|
+
rateLimitPerSecond: number;
|
|
888
|
+
listenAllGroupMessages: boolean;
|
|
889
|
+
createdBy: string; // ID của người tạo bot
|
|
890
|
+
createdAt: string; // ISO 8601
|
|
891
|
+
updatedAt: string; // ISO 8601
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
> ⚠️ `BotDto` **không** chứa `uniqueName` hay `fullName`. Dùng `client.participants.getById(bot.participantId)` để lấy tên và avatar của bot.
|
|
896
|
+
|
|
897
|
+
### Kiểu dữ liệu Request (Bot)
|
|
898
|
+
|
|
899
|
+
```ts
|
|
900
|
+
interface CreateBotRequest {
|
|
901
|
+
uniqueName: string;
|
|
902
|
+
fullName: string;
|
|
903
|
+
avatar?: MediaReference; // optional — avatar của bot
|
|
904
|
+
description?: string;
|
|
905
|
+
metadata?: Record<string, unknown>;
|
|
906
|
+
kafkaTopic: string; // KHÔNG thể thay đổi sau khi tạo
|
|
907
|
+
rateLimitPerSecond?: number;
|
|
908
|
+
listenAllGroupMessages?: boolean; // Mặc định false
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
interface UpdateBotRequest {
|
|
912
|
+
uniqueName?: string; // Có thể cập nhật sau khi tạo
|
|
913
|
+
fullName?: string;
|
|
914
|
+
avatar?: MediaReference;
|
|
915
|
+
description?: string;
|
|
916
|
+
metadata?: Record<string, unknown>;
|
|
917
|
+
rateLimitPerSecond?: number;
|
|
918
|
+
listenAllGroupMessages?: boolean;
|
|
919
|
+
// kafkaTopic KHÔNG thể cập nhật
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
## 10. Proxy — ProxyApi
|
|
926
|
+
|
|
927
|
+
**Truy cập:** `client.proxy`
|
|
928
|
+
|
|
929
|
+
Chuyển tiếp request đến các backend service đã đăng ký qua gateway.
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
// GET /api/proxy/trading/api/stocks/VN30
|
|
933
|
+
const data = await client.proxy.get<StockData>('trading', 'api/stocks/VN30');
|
|
934
|
+
|
|
935
|
+
// POST /api/proxy/trading/api/orders
|
|
936
|
+
const order = await client.proxy.post<Order>('trading', 'api/orders', {
|
|
937
|
+
symbol: 'VNM',
|
|
938
|
+
quantity: 100,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Request tùy chỉnh method
|
|
942
|
+
const result = await client.proxy.request<unknown>('myservice', 'api/resource/123', {
|
|
943
|
+
method: 'DELETE',
|
|
944
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
945
|
+
});
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## 11. Health — HealthApi
|
|
951
|
+
|
|
952
|
+
**Truy cập:** `client.health`
|
|
953
|
+
|
|
954
|
+
Các endpoint **public** (không cần JWT). Dùng để kiểm tra trạng thái server.
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
// Liveness probe — kiểm tra process có chạy không
|
|
958
|
+
const status = await client.health.live(); // "Healthy"
|
|
959
|
+
|
|
960
|
+
// Readiness probe — kiểm tra tất cả dependencies
|
|
961
|
+
const detail = await client.health.ready();
|
|
962
|
+
console.log(detail.status); // "Healthy" | "Degraded" | "Unhealthy"
|
|
963
|
+
console.log(detail.totalDuration); // ".NET TimeSpan string" như "00:00:00.0123456" — KHÔNG phải number
|
|
964
|
+
console.log(detail.results); // Record<string, HealthCheckResult>
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Kiểu dữ liệu Health
|
|
968
|
+
|
|
969
|
+
```ts
|
|
970
|
+
interface HealthStatus {
|
|
971
|
+
status: string; // "Healthy" | "Degraded" | "Unhealthy"
|
|
972
|
+
totalDuration: string; // .NET TimeSpan — "00:00:00.1234567" (KHÔNG phải number)
|
|
973
|
+
results: Record<string, HealthCheckResult>;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
interface HealthCheckResult {
|
|
977
|
+
status: string; // "Healthy" | "Degraded" | "Unhealthy"
|
|
978
|
+
description: string | null;
|
|
979
|
+
duration: string; // .NET TimeSpan — KHÔNG phải number
|
|
980
|
+
error: string | null;
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
---
|
|
985
|
+
|
|
986
|
+
## 12. Real-time: ChatHubClient
|
|
987
|
+
|
|
988
|
+
**Truy cập:** `client.realtime.chat`
|
|
989
|
+
|
|
990
|
+
Kết nối đến SignalR ChatHub (`/hubs/chat`). Xử lý tất cả sự kiện liên quan đến tin nhắn trong cuộc hội thoại.
|
|
991
|
+
|
|
992
|
+
### Kết nối
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
await client.realtime.chat.connect();
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
> Nếu đã connected, gọi `connect()` lần nữa sẽ không làm gì. SDK tự động reconnect (dùng SignalR `withAutomaticReconnect`).
|
|
999
|
+
|
|
1000
|
+
### Ngắt kết nối
|
|
1001
|
+
|
|
1002
|
+
```ts
|
|
1003
|
+
await client.realtime.chat.disconnect();
|
|
1004
|
+
// Xóa toàn bộ joinedConversations
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
> `joinedConversations` là `Set<string>` chứa các conversationId đã join. SDK dùng set này để tự động re-join sau reconnect. Có thể đọc trực tiếp: `client.realtime.chat.joinedConversations`.
|
|
1008
|
+
|
|
1009
|
+
### `rejoinConversations(): Promise<void>`
|
|
1010
|
+
|
|
1011
|
+
Re-join tất cả conversation đang track. SDK tự gọi sau reconnect; cũng có thể gọi thủ công nếu cần.
|
|
1012
|
+
|
|
1013
|
+
```ts
|
|
1014
|
+
await client.realtime.chat.rejoinConversations();
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Trạng thái kết nối
|
|
1018
|
+
|
|
1019
|
+
```ts
|
|
1020
|
+
import { HubConnectionState } from '@microsoft/signalr';
|
|
1021
|
+
|
|
1022
|
+
const state = client.realtime.chat.state;
|
|
1023
|
+
// HubConnectionState.Connected | Disconnected | Connecting | Reconnecting
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Tham gia / Rời cuộc hội thoại
|
|
1027
|
+
|
|
1028
|
+
```ts
|
|
1029
|
+
// Tham gia — bắt đầu nhận sự kiện của cuộc hội thoại
|
|
1030
|
+
await client.realtime.chat.joinConversation('conv-id');
|
|
1031
|
+
|
|
1032
|
+
// Rời
|
|
1033
|
+
await client.realtime.chat.leaveConversation('conv-id');
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
> SDK theo dõi `joinedConversations` và tự động re-join sau khi reconnect.
|
|
1037
|
+
|
|
1038
|
+
### Gửi tin nhắn qua SignalR
|
|
1039
|
+
|
|
1040
|
+
```ts
|
|
1041
|
+
const ack = await client.realtime.chat.sendMessage({
|
|
1042
|
+
conversationId: 'conv-id',
|
|
1043
|
+
blocks: [{ $type: 'text', format: 'Plain', content: 'Hello!', plainText: 'Hello!' }],
|
|
1044
|
+
clientMessageId: 'local-uuid', // optional, optimistic rendering
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
if (ack.success) {
|
|
1048
|
+
console.log('Đã gửi, messageId:', ack.messageId);
|
|
1049
|
+
} else {
|
|
1050
|
+
console.error('Lỗi:', ack.errorCode, ack.errorMessage);
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Chỉnh sửa / Xóa / Khôi phục tin nhắn qua SignalR
|
|
1055
|
+
|
|
1056
|
+
```ts
|
|
1057
|
+
// Chỉnh sửa — dùng newBlocks/newMentions (khác REST!)
|
|
1058
|
+
const editAck = await client.realtime.chat.editMessage({
|
|
1059
|
+
messageId: 'msg-id',
|
|
1060
|
+
newBlocks: [{ $type: 'text', format: 'Plain', content: 'Đã sửa', plainText: 'Đã sửa' }],
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// Xóa — truyền object, KHÔNG phải string
|
|
1064
|
+
const deleteAck = await client.realtime.chat.deleteMessage({ messageId: 'msg-id' });
|
|
1065
|
+
|
|
1066
|
+
// Khôi phục
|
|
1067
|
+
const recoverAck = await client.realtime.chat.recoverMessage({ messageId: 'msg-id' });
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
### Typing indicator
|
|
1071
|
+
|
|
1072
|
+
```ts
|
|
1073
|
+
await client.realtime.chat.startTyping('conv-id');
|
|
1074
|
+
await client.realtime.chat.stopTyping('conv-id');
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### Đánh dấu đã đọc qua SignalR
|
|
1078
|
+
|
|
1079
|
+
```ts
|
|
1080
|
+
const ack = await client.realtime.chat.markAsRead('conv-id', 'last-message-id');
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Reaction qua SignalR
|
|
1084
|
+
|
|
1085
|
+
```ts
|
|
1086
|
+
await client.realtime.chat.addReaction({ messageId: 'msg-id', conversationId: 'conv-id', emoji: '❤️' });
|
|
1087
|
+
await client.realtime.chat.removeReaction({ messageId: 'msg-id', emoji: '❤️' });
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Lắng nghe sự kiện
|
|
1091
|
+
|
|
1092
|
+
```ts
|
|
1093
|
+
// Đăng ký — trả về hàm unsubscribe
|
|
1094
|
+
const unsub = client.realtime.chat.on('messageReceived', (msg) => {
|
|
1095
|
+
console.log('Tin nhắn mới:', msg.plainTextContent);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// Hủy đăng ký
|
|
1099
|
+
unsub();
|
|
1100
|
+
|
|
1101
|
+
// Hoặc dùng .off()
|
|
1102
|
+
const handler = (msg: ChatMessageDto) => { /* ... */ };
|
|
1103
|
+
client.realtime.chat.on('messageReceived', handler);
|
|
1104
|
+
client.realtime.chat.off('messageReceived', handler);
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Toàn bộ sự kiện ChatHub
|
|
1108
|
+
|
|
1109
|
+
| Sự kiện | Payload | Mô tả |
|
|
1110
|
+
|---------|---------|-------|
|
|
1111
|
+
| `messageReceived` | `ChatMessageDto` | Tin nhắn mới trong cuộc hội thoại |
|
|
1112
|
+
| `messageUpdated` | `MessageUpdatedDto` | Tin nhắn được chỉnh sửa |
|
|
1113
|
+
| `messageDeleted` | `MessageDeletedDto` | Tin nhắn bị xóa |
|
|
1114
|
+
| `messageRecovered` | `ChatMessageDto` | Tin nhắn được khôi phục |
|
|
1115
|
+
| `reactionAdded` | `ReactionAddedDto` | Thêm reaction |
|
|
1116
|
+
| `reactionRemoved` | `ReactionRemovedDto` | Xóa reaction |
|
|
1117
|
+
| `typingStarted` | `TypingDto` | Người dùng đang gõ |
|
|
1118
|
+
| `typingStopped` | `TypingDto` | Người dùng dừng gõ |
|
|
1119
|
+
| `readReceiptUpdated` | `ReadReceiptUpdatedDto` | Cập nhật trạng thái đã đọc |
|
|
1120
|
+
| `streamStarted` | `StreamStartedDto` | Bot stream bắt đầu |
|
|
1121
|
+
| `streamStatusUpdated` | `StreamStatusUpdatedDto` | Trạng thái bot stream thay đổi |
|
|
1122
|
+
| `streamChunkReceived` | `StreamChunkReceivedDto` | Nhận thêm chunk text từ bot |
|
|
1123
|
+
| `streamCompleted` | `StreamCompletedDto` | Bot stream hoàn thành (kèm message đầy đủ) |
|
|
1124
|
+
| `streamAborted` | `StreamAbortedDto` | Bot stream bị hủy |
|
|
1125
|
+
| `error` | `HubErrorDto` | Lỗi từ server |
|
|
1126
|
+
| `reconnecting` | `Error \| undefined` | Đang reconnect |
|
|
1127
|
+
| `reconnected` | `string \| undefined` | Đã reconnect (connectionId mới) |
|
|
1128
|
+
| `disconnected` | `Error \| undefined` | Mất kết nối hẳn |
|
|
1129
|
+
|
|
1130
|
+
### Kiểu dữ liệu sự kiện (ChatHub)
|
|
1131
|
+
|
|
1132
|
+
```ts
|
|
1133
|
+
// Tin nhắn được chỉnh sửa
|
|
1134
|
+
interface MessageUpdatedDto {
|
|
1135
|
+
messageId: string;
|
|
1136
|
+
conversationId: string;
|
|
1137
|
+
blocks: Block[];
|
|
1138
|
+
plainTextContent: string | null;
|
|
1139
|
+
mentions?: Mention[];
|
|
1140
|
+
updatedAt: string; // ISO 8601
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Tin nhắn bị xóa
|
|
1144
|
+
type DeleteType = 'recall' | 'delete'; // 'recall': xóa cho tất cả; 'delete': xóa phía người gửi
|
|
1145
|
+
|
|
1146
|
+
interface MessageDeletedDto {
|
|
1147
|
+
messageId: string;
|
|
1148
|
+
conversationId: string;
|
|
1149
|
+
deleteType: DeleteType;
|
|
1150
|
+
deletedBy: string; // participantId
|
|
1151
|
+
deletedAt: string; // ISO 8601
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Reaction được thêm
|
|
1155
|
+
interface ReactionAddedDto {
|
|
1156
|
+
messageId: string;
|
|
1157
|
+
conversationId: string;
|
|
1158
|
+
participantId: string;
|
|
1159
|
+
participantName: string;
|
|
1160
|
+
emoji: string;
|
|
1161
|
+
createdAt: string; // ISO 8601
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Reaction được xóa — chú ý field là removedAt, KHÔNG phải createdAt
|
|
1165
|
+
interface ReactionRemovedDto {
|
|
1166
|
+
messageId: string;
|
|
1167
|
+
conversationId: string;
|
|
1168
|
+
participantId: string;
|
|
1169
|
+
participantName: string;
|
|
1170
|
+
emoji: string;
|
|
1171
|
+
removedAt: string; // ISO 8601 — khác với ReactionAddedDto.createdAt
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Typing indicator
|
|
1175
|
+
interface TypingDto {
|
|
1176
|
+
conversationId: string;
|
|
1177
|
+
participantId: string;
|
|
1178
|
+
participantName: string;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Trạng thái đã đọc
|
|
1182
|
+
interface ReadReceiptUpdatedDto {
|
|
1183
|
+
conversationId: string;
|
|
1184
|
+
participantId: string;
|
|
1185
|
+
lastReadMessageId: string;
|
|
1186
|
+
readAt: string; // ISO 8601
|
|
1187
|
+
}
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
### Bot streaming
|
|
1191
|
+
|
|
1192
|
+
Khi bot streaming, **không có** `messageReceived` — thay vào đó nhận chuỗi sự kiện sau:
|
|
1193
|
+
|
|
1194
|
+
```
|
|
1195
|
+
streamStarted → streamStatusUpdated* → streamChunkReceived* → streamCompleted
|
|
1196
|
+
→ streamAborted
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
```ts
|
|
1200
|
+
const chunks: string[] = [];
|
|
1201
|
+
|
|
1202
|
+
client.realtime.chat.on('streamStarted', (dto) => {
|
|
1203
|
+
console.log('Bot bắt đầu trả lời, streamId:', dto.streamId);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
client.realtime.chat.on('streamChunkReceived', (dto) => {
|
|
1207
|
+
chunks.push(dto.text);
|
|
1208
|
+
// Hiển thị progressive: chunks.join('')
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
client.realtime.chat.on('streamCompleted', (dto) => {
|
|
1212
|
+
console.log('Tin nhắn hoàn chỉnh:', dto.message);
|
|
1213
|
+
chunks.length = 0; // reset
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
client.realtime.chat.on('streamAborted', (dto) => {
|
|
1217
|
+
console.error('Stream bị hủy:', dto.reason);
|
|
1218
|
+
});
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## 13. Real-time: NotificationHubClient
|
|
1224
|
+
|
|
1225
|
+
**Truy cập:** `client.realtime.notifications`
|
|
1226
|
+
|
|
1227
|
+
Kết nối đến SignalR NotificationHub (`/hubs/notifications`). Nhận thông báo cấp user (không cần tham gia conversation).
|
|
1228
|
+
|
|
1229
|
+
### Trạng thái kết nối
|
|
1230
|
+
|
|
1231
|
+
```ts
|
|
1232
|
+
import { HubConnectionState } from '@microsoft/signalr';
|
|
1233
|
+
|
|
1234
|
+
const state = client.realtime.notifications.state;
|
|
1235
|
+
// HubConnectionState.Connected | Disconnected | Connecting | Reconnecting
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### Kết nối
|
|
1239
|
+
|
|
1240
|
+
```ts
|
|
1241
|
+
await client.realtime.notifications.connect();
|
|
1242
|
+
// Khi connect, server tự động đánh dấu user là Online
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### Ngắt kết nối
|
|
1246
|
+
|
|
1247
|
+
```ts
|
|
1248
|
+
await client.realtime.notifications.disconnect();
|
|
1249
|
+
// Server tự động đánh dấu user là Offline
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
> `subscribedPresenceIds` là `Set<string>` chứa các participantId đang theo dõi presence. SDK tự động re-subscribe sau reconnect. Có thể đọc trực tiếp: `client.realtime.notifications.subscribedPresenceIds`.
|
|
1253
|
+
|
|
1254
|
+
### `resubscribePresence(): Promise<void>`
|
|
1255
|
+
|
|
1256
|
+
Re-subscribe presence cho tất cả participantId đang track. SDK tự gọi sau reconnect; cũng có thể gọi thủ công nếu cần.
|
|
1257
|
+
|
|
1258
|
+
```ts
|
|
1259
|
+
await client.realtime.notifications.resubscribePresence();
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### Theo dõi trạng thái online (Presence)
|
|
1263
|
+
|
|
1264
|
+
```ts
|
|
1265
|
+
// Đăng ký theo dõi tối đa 200 participant mỗi lần
|
|
1266
|
+
await client.realtime.notifications.subscribeToPresence(['user-1', 'user-2', 'user-3']);
|
|
1267
|
+
|
|
1268
|
+
// Server gửi PresenceState ngay lập tức sau khi subscribe
|
|
1269
|
+
client.realtime.notifications.on('presenceState', (states) => {
|
|
1270
|
+
states.forEach(s => {
|
|
1271
|
+
console.log(s.participantId, s.isOnline, s.lastSeenAt);
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Sau đó nhận cập nhật real-time
|
|
1276
|
+
client.realtime.notifications.on('presenceChanged', (dto) => {
|
|
1277
|
+
console.log(dto.participantId, 'is now', dto.isOnline ? 'online' : 'offline');
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// Hủy theo dõi — cũng giới hạn tối đa 200 participant mỗi lần
|
|
1281
|
+
await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
> SDK tự động re-subscribe sau khi reconnect.
|
|
1285
|
+
|
|
1286
|
+
### Toàn bộ sự kiện NotificationHub
|
|
1287
|
+
|
|
1288
|
+
| Sự kiện | Payload | Mô tả |
|
|
1289
|
+
|---------|---------|-------|
|
|
1290
|
+
| `presenceState` | `PresenceStateItem[]` | Trạng thái online ban đầu sau subscribe |
|
|
1291
|
+
| `presenceChanged` | `PresenceChangedDto` | Thay đổi trạng thái online |
|
|
1292
|
+
| `newMessageNotification` | `NewMessageNotificationDto` | Có tin nhắn mới (thông báo cho tất cả participants trừ người gửi) |
|
|
1293
|
+
| `mentionedNotification` | `MentionedNotificationDto` | Bị mention trong tin nhắn |
|
|
1294
|
+
| `unreadCountChanged` | `UnreadCountChangedDto` | Số tin chưa đọc thay đổi |
|
|
1295
|
+
| `conversationCreated` | `ConversationCreatedDto` | Cuộc hội thoại mới được tạo (hoặc bị thêm vào) |
|
|
1296
|
+
| `conversationUpdated` | `ConversationUpdatedDto` | Metadata cuộc hội thoại thay đổi |
|
|
1297
|
+
| `participantJoined` | `ParticipantJoinedDto` | Thành viên mới tham gia |
|
|
1298
|
+
| `participantLeft` | `ParticipantLeftDto` | Thành viên rời nhóm |
|
|
1299
|
+
| `conversationPinned` | `ConversationPinnedDto` | Cuộc hội thoại được pin |
|
|
1300
|
+
| `conversationUnpinned` | `ConversationUnpinnedDto` | Cuộc hội thoại được unpin |
|
|
1301
|
+
| `readStatusChanged` | `ReadStatusChangedDto` | Trạng thái đọc thay đổi |
|
|
1302
|
+
| `error` | `HubErrorDto` | Lỗi từ server |
|
|
1303
|
+
| `reconnecting` | `Error \| undefined` | Đang reconnect |
|
|
1304
|
+
| `reconnected` | `string \| undefined` | Đã reconnect |
|
|
1305
|
+
| `disconnected` | `Error \| undefined` | Mất kết nối |
|
|
1306
|
+
|
|
1307
|
+
### Kiểu dữ liệu sự kiện (NotificationHub)
|
|
1308
|
+
|
|
1309
|
+
```ts
|
|
1310
|
+
// Trạng thái online — trả về ngay sau subscribe
|
|
1311
|
+
interface PresenceStateItem {
|
|
1312
|
+
participantId: string;
|
|
1313
|
+
isOnline: boolean;
|
|
1314
|
+
lastSeenAt?: string; // ISO 8601, undefined khi đang online
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Thay đổi trạng thái online — real-time update
|
|
1318
|
+
interface PresenceChangedDto {
|
|
1319
|
+
participantId: string;
|
|
1320
|
+
isOnline: boolean;
|
|
1321
|
+
lastSeenAt?: string; // ISO 8601, undefined khi đang online
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
interface NewMessageNotificationDto {
|
|
1325
|
+
conversationId: string;
|
|
1326
|
+
conversationType: ConversationType;
|
|
1327
|
+
conversationName?: string;
|
|
1328
|
+
messageId: string;
|
|
1329
|
+
senderId: string;
|
|
1330
|
+
senderName: string;
|
|
1331
|
+
senderAvatar?: MediaReference;
|
|
1332
|
+
contentPreview: string;
|
|
1333
|
+
sentAt: string; // ISO 8601
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Chú ý: KHÔNG có senderAvatar (khác NewMessageNotificationDto)
|
|
1337
|
+
interface MentionedNotificationDto {
|
|
1338
|
+
conversationId: string;
|
|
1339
|
+
messageId: string;
|
|
1340
|
+
senderId: string;
|
|
1341
|
+
senderName: string;
|
|
1342
|
+
contentPreview: string;
|
|
1343
|
+
sentAt: string; // ISO 8601
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
interface UnreadCountChangedDto {
|
|
1347
|
+
conversationId: string;
|
|
1348
|
+
unreadCount: number;
|
|
1349
|
+
lastMessageAt: string; // ISO 8601
|
|
1350
|
+
lastMessageId: string;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
interface ConversationCreatedDto {
|
|
1354
|
+
conversationId: string;
|
|
1355
|
+
type: ConversationType;
|
|
1356
|
+
name?: string;
|
|
1357
|
+
avatar?: MediaReference;
|
|
1358
|
+
participantCount: number;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
interface ConversationUpdatedDto {
|
|
1362
|
+
conversationId: string;
|
|
1363
|
+
type: ConversationType;
|
|
1364
|
+
name?: string;
|
|
1365
|
+
avatar?: MediaReference | null; // null = avatar đã bị xóa
|
|
1366
|
+
participantCount: number;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
interface ParticipantJoinedDto {
|
|
1370
|
+
conversationId: string;
|
|
1371
|
+
participantId: string;
|
|
1372
|
+
participantName: string;
|
|
1373
|
+
participantAvatar?: MediaReference;
|
|
1374
|
+
changedAt: string; // ISO 8601
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Cùng shape với ParticipantJoinedDto
|
|
1378
|
+
interface ParticipantLeftDto {
|
|
1379
|
+
conversationId: string;
|
|
1380
|
+
participantId: string;
|
|
1381
|
+
participantName: string;
|
|
1382
|
+
participantAvatar?: MediaReference;
|
|
1383
|
+
changedAt: string; // ISO 8601
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
interface ConversationPinnedDto {
|
|
1387
|
+
conversationId: string;
|
|
1388
|
+
pinnedAt: string; // ISO 8601
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Chú ý: KHÔNG có pinnedAt (khác ConversationPinnedDto)
|
|
1392
|
+
interface ConversationUnpinnedDto {
|
|
1393
|
+
conversationId: string;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
interface ReadStatusChangedDto {
|
|
1397
|
+
conversationId: string;
|
|
1398
|
+
lastReadMessageId?: string;
|
|
1399
|
+
unreadCount: number;
|
|
1400
|
+
markedAsUnread: boolean; // true khi user chủ động đánh dấu chưa đọc
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
---
|
|
1405
|
+
|
|
1406
|
+
## 14. ReconnectionManager
|
|
1407
|
+
|
|
1408
|
+
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
|
+
|
|
1410
|
+
```ts
|
|
1411
|
+
import { ReconnectionManager } from 'chat-client-sdk';
|
|
1412
|
+
|
|
1413
|
+
const manager = new ReconnectionManager({
|
|
1414
|
+
chatHub: client.realtime.chat,
|
|
1415
|
+
notificationHub: client.realtime.notifications,
|
|
1416
|
+
onTokenExpired: async () => {
|
|
1417
|
+
// Gọi API refresh token của ứng dụng
|
|
1418
|
+
const newToken = await refreshToken();
|
|
1419
|
+
if (newToken) {
|
|
1420
|
+
client.setToken(newToken); // Cập nhật token vào SDK
|
|
1421
|
+
}
|
|
1422
|
+
return newToken; // Trả về null để hủy reconnect
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
manager.start(); // Bắt đầu quản lý — idempotent, gọi nhiều lần an toàn
|
|
1427
|
+
|
|
1428
|
+
// Khi không còn cần
|
|
1429
|
+
manager.stop();
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
**Chiến lược backoff:** 3 lần thử với delay 2s → 5s → 10s.
|
|
1433
|
+
|
|
1434
|
+
**Phát hiện token hết hạn:** Kiểm tra error message có chứa `"401"` hoặc `"unauthorized"`.
|
|
1435
|
+
|
|
1436
|
+
---
|
|
1437
|
+
|
|
1438
|
+
## 15. Hệ thống Block (nội dung tin nhắn)
|
|
1439
|
+
|
|
1440
|
+
`Block` là **discriminated union** — mỗi block phân biệt bằng field `$type`.
|
|
1441
|
+
|
|
1442
|
+
### Các loại Block
|
|
1443
|
+
|
|
1444
|
+
| `$type` | Interface | Mô tả |
|
|
1445
|
+
|---------|-----------|-------|
|
|
1446
|
+
| `"text"` | `TextBlock` | Văn bản (Plain, Markdown, Html, ProseMirrorJson) |
|
|
1447
|
+
| `"image"` | `ImageBlock` | Hình ảnh |
|
|
1448
|
+
| `"video"` | `VideoBlock` | Video |
|
|
1449
|
+
| `"audio"` | `AudioBlock` | Âm thanh |
|
|
1450
|
+
| `"file"` | `FileBlock` | Tệp tin |
|
|
1451
|
+
| `"linkPreview"` | `LinkPreviewBlock` | Preview URL |
|
|
1452
|
+
| `"embed"` | `EmbedBlock` | Nhúng nội dung (iframe) |
|
|
1453
|
+
| `"location"` | `LocationBlock` | Vị trí địa lý |
|
|
1454
|
+
| `"contact"` | `ContactBlock` | Thông tin liên hệ |
|
|
1455
|
+
| `"choice"` | `ChoiceBlock` | Câu hỏi / lựa chọn (bot) |
|
|
1456
|
+
| `"card"` | `CardBlock` | Thẻ thông tin có button |
|
|
1457
|
+
| `"carousel"` | `CarouselBlock` | Nhiều card liên tiếp |
|
|
1458
|
+
| `"divider"` | `DividerBlock` | Đường phân cách |
|
|
1459
|
+
| `"custom"` | `CustomBlock` | Block tùy chỉnh |
|
|
1460
|
+
|
|
1461
|
+
### Interfaces tham chiếu đầy đủ
|
|
1462
|
+
|
|
1463
|
+
```ts
|
|
1464
|
+
interface TextBlock {
|
|
1465
|
+
$type: 'text';
|
|
1466
|
+
format: TextFormat; // 'Plain' | 'Markdown' | 'Html' | 'ProseMirrorJson'
|
|
1467
|
+
content: string;
|
|
1468
|
+
plainText: string | null;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
interface ImageBlock {
|
|
1472
|
+
$type: 'image';
|
|
1473
|
+
source: MediaReference;
|
|
1474
|
+
thumbnail: MediaReference | null;
|
|
1475
|
+
altText: string | null;
|
|
1476
|
+
width: number | null;
|
|
1477
|
+
height: number | null;
|
|
1478
|
+
caption: string | null;
|
|
1479
|
+
fileSizeBytes: number | null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
interface VideoBlock {
|
|
1483
|
+
$type: 'video';
|
|
1484
|
+
source: MediaReference;
|
|
1485
|
+
thumbnail: MediaReference | null;
|
|
1486
|
+
caption: string | null;
|
|
1487
|
+
durationSeconds: number | null;
|
|
1488
|
+
mimeType: string | null;
|
|
1489
|
+
fileSizeBytes: number | null;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
interface AudioBlock {
|
|
1493
|
+
$type: 'audio';
|
|
1494
|
+
source: MediaReference;
|
|
1495
|
+
durationSeconds: number | null;
|
|
1496
|
+
mimeType: string | null;
|
|
1497
|
+
transcript: string | null; // Bản ghi âm thanh (nếu có)
|
|
1498
|
+
fileSizeBytes: number | null;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
interface FileBlock {
|
|
1502
|
+
$type: 'file';
|
|
1503
|
+
source: MediaReference;
|
|
1504
|
+
fileName: string;
|
|
1505
|
+
mimeType: string | null;
|
|
1506
|
+
fileSizeBytes: number | null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
interface LinkPreviewBlock {
|
|
1510
|
+
$type: 'linkPreview';
|
|
1511
|
+
url: string;
|
|
1512
|
+
title: string | null;
|
|
1513
|
+
description: string | null;
|
|
1514
|
+
image: MediaReference | null;
|
|
1515
|
+
siteName: string | null;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
interface EmbedBlock {
|
|
1519
|
+
$type: 'embed';
|
|
1520
|
+
url: string;
|
|
1521
|
+
html: string | null; // HTML nhúng (iframe snippet)
|
|
1522
|
+
width: number | null;
|
|
1523
|
+
height: number | null;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
interface LocationBlock {
|
|
1527
|
+
$type: 'location';
|
|
1528
|
+
latitude: number;
|
|
1529
|
+
longitude: number;
|
|
1530
|
+
name: string | null;
|
|
1531
|
+
address: string | null;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
interface ContactBlock {
|
|
1535
|
+
$type: 'contact';
|
|
1536
|
+
displayName: string;
|
|
1537
|
+
phone: string | null;
|
|
1538
|
+
email: string | null;
|
|
1539
|
+
organization: string | null;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
interface ChoiceBlock {
|
|
1543
|
+
$type: 'choice';
|
|
1544
|
+
prompt: string; // Câu hỏi hiển thị cho user
|
|
1545
|
+
options: ChoiceOption[];
|
|
1546
|
+
mode: ChoiceMode; // 'Single' | 'Multiple'
|
|
1547
|
+
submitted: boolean; // true sau khi user đã submit lựa chọn
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
interface CardBlock {
|
|
1551
|
+
$type: 'card';
|
|
1552
|
+
title: string | null;
|
|
1553
|
+
subtitle: string | null;
|
|
1554
|
+
image: MediaReference | null;
|
|
1555
|
+
fields: CardField[] | null;
|
|
1556
|
+
buttons: ActionButton[] | null;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
interface CarouselBlock {
|
|
1560
|
+
$type: 'carousel';
|
|
1561
|
+
cards: CardBlock[]; // Danh sách card trình chiếu ngang
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
interface DividerBlock {
|
|
1565
|
+
$type: 'divider'; // Đường phân cách — không có field nào khác
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
interface CustomBlock {
|
|
1569
|
+
$type: 'custom';
|
|
1570
|
+
type: string; // Discriminator tùy chỉnh của ứng dụng
|
|
1571
|
+
data: Record<string, unknown> | null;
|
|
1572
|
+
}
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
### Type Guards
|
|
1576
|
+
|
|
1577
|
+
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
|
+
|
|
1579
|
+
```ts
|
|
1580
|
+
import { isTextBlock, isImageBlock, isFileBlock, isCardBlock } from 'chat-client-sdk';
|
|
1581
|
+
|
|
1582
|
+
for (const block of message.blocks) {
|
|
1583
|
+
if (isTextBlock(block)) {
|
|
1584
|
+
console.log(block.content); // TypeScript biết đây là TextBlock
|
|
1585
|
+
} else if (isImageBlock(block)) {
|
|
1586
|
+
console.log(block.source.url); // ImageBlock
|
|
1587
|
+
} else if (isFileBlock(block)) {
|
|
1588
|
+
console.log(block.fileName, block.fileSizeBytes);
|
|
1589
|
+
} else if (isCardBlock(block)) {
|
|
1590
|
+
block.buttons?.forEach(btn => console.log(btn.label, btn.action));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
Danh sách đầy đủ: `isTextBlock`, `isImageBlock`, `isVideoBlock`, `isAudioBlock`, `isFileBlock`, `isLinkPreviewBlock`, `isEmbedBlock`, `isLocationBlock`, `isContactBlock`, `isChoiceBlock`, `isCardBlock`, `isCarouselBlock`, `isDividerBlock`, `isCustomBlock`.
|
|
1596
|
+
|
|
1597
|
+
### Ví dụ tạo block
|
|
1598
|
+
|
|
1599
|
+
```ts
|
|
1600
|
+
// Văn bản Markdown
|
|
1601
|
+
const textBlock: TextBlock = {
|
|
1602
|
+
$type: 'text',
|
|
1603
|
+
format: 'Markdown',
|
|
1604
|
+
content: '**Hello** _world_!',
|
|
1605
|
+
plainText: 'Hello world!',
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
// Hình ảnh đã upload
|
|
1609
|
+
const imageBlock: ImageBlock = {
|
|
1610
|
+
$type: 'image',
|
|
1611
|
+
source: { storageKey: '01HXABCDEF/photo.jpg' },
|
|
1612
|
+
thumbnail: null,
|
|
1613
|
+
altText: 'Ảnh chụp',
|
|
1614
|
+
width: 1920,
|
|
1615
|
+
height: 1080,
|
|
1616
|
+
caption: 'Ảnh mô tả',
|
|
1617
|
+
fileSizeBytes: 204800,
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
// Gửi tin nhắn kèm file
|
|
1621
|
+
const fileBlock: FileBlock = {
|
|
1622
|
+
$type: 'file',
|
|
1623
|
+
source: { storageKey: '01HXABCDEF/report.pdf' },
|
|
1624
|
+
fileName: 'report.pdf',
|
|
1625
|
+
mimeType: 'application/pdf',
|
|
1626
|
+
fileSizeBytes: 1048576,
|
|
1627
|
+
};
|
|
1628
|
+
```
|
|
1629
|
+
|
|
1630
|
+
### TextFormat
|
|
1631
|
+
|
|
1632
|
+
| Giá trị | Mô tả |
|
|
1633
|
+
|---------|-------|
|
|
1634
|
+
| `'Plain'` | Văn bản thuần |
|
|
1635
|
+
| `'Markdown'` | Markdown |
|
|
1636
|
+
| `'Html'` | HTML (render trong WebView) |
|
|
1637
|
+
| `'ProseMirrorJson'` | Rich text dạng JSON (ProseMirror) |
|
|
1638
|
+
|
|
1639
|
+
### ButtonAction / ButtonStyle (dùng trong CardBlock)
|
|
1640
|
+
|
|
1641
|
+
```ts
|
|
1642
|
+
type ButtonAction = 'Postback' | 'Url' | 'Call' | 'Copy';
|
|
1643
|
+
type ButtonStyle = 'Default' | 'Primary' | 'Danger';
|
|
1644
|
+
|
|
1645
|
+
interface ActionButton {
|
|
1646
|
+
label: string;
|
|
1647
|
+
action: ButtonAction;
|
|
1648
|
+
value: string | null;
|
|
1649
|
+
style: ButtonStyle;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
interface CardField {
|
|
1653
|
+
label: string;
|
|
1654
|
+
value: string;
|
|
1655
|
+
}
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
### ChoiceMode / ChoiceOption / ChoiceBlock
|
|
1659
|
+
|
|
1660
|
+
```ts
|
|
1661
|
+
type ChoiceMode = 'Single' | 'Multiple';
|
|
1662
|
+
|
|
1663
|
+
interface ChoiceOption {
|
|
1664
|
+
label: string;
|
|
1665
|
+
value: string;
|
|
1666
|
+
selected: boolean;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
interface ChoiceBlock {
|
|
1670
|
+
$type: 'choice';
|
|
1671
|
+
prompt: string; // Câu hỏi hiển thị cho user
|
|
1672
|
+
options: ChoiceOption[];
|
|
1673
|
+
mode: ChoiceMode;
|
|
1674
|
+
submitted: boolean; // true sau khi user đã submit lựa chọn
|
|
1675
|
+
}
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### QuickReply
|
|
1679
|
+
|
|
1680
|
+
Nút bấm nhanh hiển thị bên dưới tin nhắn bot:
|
|
1681
|
+
|
|
1682
|
+
```ts
|
|
1683
|
+
interface QuickReply {
|
|
1684
|
+
label: string; // Text hiển thị
|
|
1685
|
+
action: QuickReplyAction; // 'SendText' | 'Postback' | 'Url'
|
|
1686
|
+
value: string | null;
|
|
1687
|
+
icon: string | null;
|
|
1688
|
+
}
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
### Mention
|
|
1692
|
+
|
|
1693
|
+
```ts
|
|
1694
|
+
interface Mention {
|
|
1695
|
+
type: MentionType; // 'User' | 'All' | 'Here' | 'Role'
|
|
1696
|
+
targetId: string | null; // User ID (null cho All/Here)
|
|
1697
|
+
displayName: string;
|
|
1698
|
+
offset: number | null; // Vị trí trong plainText
|
|
1699
|
+
length: number | null;
|
|
1700
|
+
}
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
---
|
|
1704
|
+
|
|
1705
|
+
## 16. Toàn bộ kiểu dữ liệu (Types)
|
|
1706
|
+
|
|
1707
|
+
### PaginatedResult (cursor-based)
|
|
1708
|
+
|
|
1709
|
+
```ts
|
|
1710
|
+
interface PaginatedResult<T> {
|
|
1711
|
+
items: T[];
|
|
1712
|
+
nextCursor: string | null; // PHẢI là string — KHÔNG cast sang number
|
|
1713
|
+
hasMore: boolean;
|
|
1714
|
+
}
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
### PagedResult (page-based)
|
|
1718
|
+
|
|
1719
|
+
```ts
|
|
1720
|
+
interface PagedResult<T> {
|
|
1721
|
+
items: T[];
|
|
1722
|
+
page: number;
|
|
1723
|
+
pageSize: number;
|
|
1724
|
+
totalCount: number;
|
|
1725
|
+
}
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
### MediaReference
|
|
1729
|
+
|
|
1730
|
+
```ts
|
|
1731
|
+
interface MediaReference {
|
|
1732
|
+
storageKey?: string; // Key nội bộ — dùng FileApi.getDownloadUrl() để tạo URL
|
|
1733
|
+
url?: string; // URL bên ngoài (không qua storage)
|
|
1734
|
+
}
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
### ChatMessageDto (SignalR version)
|
|
1738
|
+
|
|
1739
|
+
Khác với `MessageDto` (REST):
|
|
1740
|
+
|
|
1741
|
+
```ts
|
|
1742
|
+
interface ChatMessageDto {
|
|
1743
|
+
id: string;
|
|
1744
|
+
conversationId: string;
|
|
1745
|
+
senderId: string;
|
|
1746
|
+
senderName: string; // Có ở SignalR, không có ở REST
|
|
1747
|
+
senderAvatar?: MediaReference; // Có ở SignalR, không có ở REST
|
|
1748
|
+
senderType: SenderType; // Có ở cả REST và SignalR
|
|
1749
|
+
blocks: Block[];
|
|
1750
|
+
plainTextContent: string | null; // Tên khác với REST (plainTextIndex)
|
|
1751
|
+
replyToMessageId: string | null;
|
|
1752
|
+
quickReplies: QuickReply[] | null;
|
|
1753
|
+
isEdited: boolean;
|
|
1754
|
+
reactions: ReactionGroupDto[] | null; // Flat array, khác REST
|
|
1755
|
+
mentions: Mention[] | null;
|
|
1756
|
+
createdAt: string;
|
|
1757
|
+
updatedAt: string | null;
|
|
1758
|
+
clientMessageId: string | null;
|
|
1759
|
+
systemEvent: SystemEventDto | null;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Reaction group (SignalR version — flat array, KHÔNG gói trong ReactionSummary)
|
|
1763
|
+
// So sánh với REST: ReactionSummary { groups: ReactionGroup[], totalCount }
|
|
1764
|
+
interface ReactionGroupDto {
|
|
1765
|
+
emoji: string;
|
|
1766
|
+
count: number;
|
|
1767
|
+
participantIds: string[]; // Danh sách participant đã react
|
|
1768
|
+
}
|
|
1769
|
+
```
|
|
1770
|
+
|
|
1771
|
+
### SystemEvent
|
|
1772
|
+
|
|
1773
|
+
```ts
|
|
1774
|
+
type SystemEventType =
|
|
1775
|
+
| 'ConversationRenamed' // Đổi tên nhóm
|
|
1776
|
+
| 'AvatarChanged' // Đổi avatar
|
|
1777
|
+
| 'MemberAdded' // Thêm thành viên
|
|
1778
|
+
| 'MemberRemoved' // Xóa thành viên
|
|
1779
|
+
| 'MemberLeft' // Thành viên tự rời
|
|
1780
|
+
| 'Custom';
|
|
1781
|
+
|
|
1782
|
+
interface SystemEventInfo {
|
|
1783
|
+
type: SystemEventType;
|
|
1784
|
+
actorId: string | null; // null khi MemberLeft (tự rời)
|
|
1785
|
+
targetIds?: string[];
|
|
1786
|
+
metadata?: Record<string, unknown>;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// SignalR (ChatHub) version — kế thừa SystemEventInfo, thêm display names
|
|
1790
|
+
interface SystemEventDto extends SystemEventInfo {
|
|
1791
|
+
actorName?: string; // Tên hiển thị của actor
|
|
1792
|
+
targetNames?: string[]; // Tên hiển thị của các target participants
|
|
1793
|
+
}
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
### HubErrorDto
|
|
1797
|
+
|
|
1798
|
+
```ts
|
|
1799
|
+
interface HubErrorDto {
|
|
1800
|
+
code: string;
|
|
1801
|
+
message: string;
|
|
1802
|
+
details?: Record<string, unknown>;
|
|
1803
|
+
}
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
### Streaming DTOs (ChatHub)
|
|
1807
|
+
|
|
1808
|
+
```ts
|
|
1809
|
+
interface StreamStartedDto {
|
|
1810
|
+
streamId: string;
|
|
1811
|
+
messageId: string;
|
|
1812
|
+
conversationId: string;
|
|
1813
|
+
senderId: string;
|
|
1814
|
+
senderName: string;
|
|
1815
|
+
senderAvatar?: MediaReference;
|
|
1816
|
+
replyToMessageId?: string;
|
|
1817
|
+
startedAt: string; // ISO 8601
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
interface StreamStatusUpdatedDto {
|
|
1821
|
+
streamId: string;
|
|
1822
|
+
messageId: string;
|
|
1823
|
+
conversationId: string;
|
|
1824
|
+
status: string; // e.g. "thinking", "searching"
|
|
1825
|
+
detail?: string;
|
|
1826
|
+
updatedAt: string; // ISO 8601
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
interface StreamChunkReceivedDto {
|
|
1830
|
+
streamId: string;
|
|
1831
|
+
messageId: string;
|
|
1832
|
+
conversationId: string;
|
|
1833
|
+
text: string; // Chunk text để append vào placeholder
|
|
1834
|
+
chunkIndex: number; // Zero-based index
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
interface StreamCompletedDto {
|
|
1838
|
+
streamId: string;
|
|
1839
|
+
messageId: string;
|
|
1840
|
+
conversationId: string;
|
|
1841
|
+
message: ChatMessageDto; // Tin nhắn hoàn chỉnh (SignalR, không phải REST MessageDto)
|
|
1842
|
+
completedAt: string; // ISO 8601
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
interface StreamAbortedDto {
|
|
1846
|
+
streamId: string;
|
|
1847
|
+
messageId: string;
|
|
1848
|
+
conversationId: string;
|
|
1849
|
+
reason: string;
|
|
1850
|
+
abortedAt: string; // ISO 8601
|
|
1851
|
+
}
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
### Ack types (return value của Hub methods)
|
|
1855
|
+
|
|
1856
|
+
Mỗi Hub method client→server trả về một ack object với `success: boolean` và optional error fields:
|
|
1857
|
+
|
|
1858
|
+
```ts
|
|
1859
|
+
interface SendMessageAck {
|
|
1860
|
+
success: boolean;
|
|
1861
|
+
messageId?: string;
|
|
1862
|
+
clientMessageId?: string;
|
|
1863
|
+
timestamp?: string; // ISO 8601
|
|
1864
|
+
errorCode?: string;
|
|
1865
|
+
errorMessage?: string;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
interface EditMessageAck {
|
|
1869
|
+
success: boolean;
|
|
1870
|
+
messageId?: string;
|
|
1871
|
+
editedAt?: string; // ISO 8601
|
|
1872
|
+
errorCode?: string;
|
|
1873
|
+
errorMessage?: string;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
interface DeleteMessageAck {
|
|
1877
|
+
success: boolean;
|
|
1878
|
+
messageId?: string;
|
|
1879
|
+
deletedAt?: string; // ISO 8601
|
|
1880
|
+
errorCode?: string;
|
|
1881
|
+
errorMessage?: string;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
interface RecoverMessageAck {
|
|
1885
|
+
success: boolean;
|
|
1886
|
+
messageId?: string;
|
|
1887
|
+
errorCode?: string;
|
|
1888
|
+
errorMessage?: string;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
interface ReactionAck {
|
|
1892
|
+
success: boolean;
|
|
1893
|
+
messageId?: string;
|
|
1894
|
+
emoji?: string;
|
|
1895
|
+
errorCode?: string;
|
|
1896
|
+
errorMessage?: string;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
interface MarkAsReadAck {
|
|
1900
|
+
success: boolean;
|
|
1901
|
+
conversationId?: string;
|
|
1902
|
+
errorCode?: string;
|
|
1903
|
+
errorMessage?: string;
|
|
1904
|
+
}
|
|
1905
|
+
```
|
|
1906
|
+
|
|
1907
|
+
---
|
|
1908
|
+
|
|
1909
|
+
## 17. Xử lý lỗi — ChatApiError
|
|
1910
|
+
|
|
1911
|
+
Tất cả lỗi HTTP đều throw `ChatApiError` (kế thừa `Error`).
|
|
1912
|
+
|
|
1913
|
+
```ts
|
|
1914
|
+
import { ChatApiError } from 'chat-client-sdk';
|
|
1915
|
+
|
|
1916
|
+
try {
|
|
1917
|
+
await client.messages.send('conv-id', { blocks: [...] });
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
if (err instanceof ChatApiError) {
|
|
1920
|
+
console.error(`HTTP ${err.status}: ${err.title}`);
|
|
1921
|
+
console.error('Chi tiết:', err.detail);
|
|
1922
|
+
|
|
1923
|
+
switch (err.status) {
|
|
1924
|
+
case 401: // Unauthorized — token hết hạn/không hợp lệ
|
|
1925
|
+
await refreshAndRetry();
|
|
1926
|
+
break;
|
|
1927
|
+
case 403: // Forbidden — không có quyền
|
|
1928
|
+
break;
|
|
1929
|
+
case 404: // Not Found
|
|
1930
|
+
break;
|
|
1931
|
+
case 409: // Conflict — ví dụ: đã react emoji đó rồi
|
|
1932
|
+
break;
|
|
1933
|
+
case 429: // Rate Limited
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
### Thuộc tính ChatApiError
|
|
1941
|
+
|
|
1942
|
+
| Thuộc tính | Kiểu | Mô tả |
|
|
1943
|
+
|------------|------|-------|
|
|
1944
|
+
| `status` | `number` | HTTP status code |
|
|
1945
|
+
| `title` | `string` | Tiêu đề lỗi (từ RFC 7807 `title`) |
|
|
1946
|
+
| `detail` | `string \| undefined` | Chi tiết lỗi (từ RFC 7807 `detail`) |
|
|
1947
|
+
| `message` | `string` | `detail ?? title` (kế thừa từ `Error`) |
|
|
1948
|
+
| `name` | `string` | Luôn là `"ChatApiError"` |
|
|
1949
|
+
|
|
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.
|
|
1951
|
+
|
|
1952
|
+
### Static methods
|
|
1953
|
+
|
|
1954
|
+
#### `ChatApiError.fromResponse(response: Response): Promise<ChatApiError>`
|
|
1955
|
+
|
|
1956
|
+
Parse lỗi từ HTTP `Response`. SDK gọi nội bộ — bạn không cần gọi trực tiếp.
|
|
1957
|
+
|
|
1958
|
+
#### `ChatApiError.fromProblemDetails(pd: ProblemDetails): ChatApiError`
|
|
1959
|
+
|
|
1960
|
+
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
|
+
|
|
1962
|
+
```ts
|
|
1963
|
+
import { ChatApiError } from 'chat-client-sdk';
|
|
1964
|
+
import type { ProblemDetails } from 'chat-client-sdk';
|
|
1965
|
+
|
|
1966
|
+
const pd: ProblemDetails = { status: 404, title: 'Not Found', detail: 'Conversation not found' };
|
|
1967
|
+
const error = ChatApiError.fromProblemDetails(pd);
|
|
1968
|
+
console.log(error.status); // 404
|
|
1969
|
+
console.log(error.message); // "Conversation not found"
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
### ProblemDetails
|
|
1973
|
+
|
|
1974
|
+
Kiểu dữ liệu RFC 7807 mà server trả về khi có lỗi:
|
|
1975
|
+
|
|
1976
|
+
```ts
|
|
1977
|
+
interface ProblemDetails {
|
|
1978
|
+
status: number;
|
|
1979
|
+
title: string;
|
|
1980
|
+
detail?: string;
|
|
1981
|
+
type?: string; // URI reference — thường không dùng trực tiếp
|
|
1982
|
+
}
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
---
|
|
1986
|
+
|
|
1987
|
+
### Sử dụng hub client độc lập (không qua ChatClient)
|
|
1988
|
+
|
|
1989
|
+
Power users có thể khởi tạo `ChatHubClient` hoặc `NotificationHubClient` trực tiếp, không qua `ChatClient` facade:
|
|
1990
|
+
|
|
1991
|
+
```ts
|
|
1992
|
+
import { ChatHubClient, NotificationHubClient } from 'chat-client-sdk';
|
|
1993
|
+
import { LogLevel } from '@microsoft/signalr';
|
|
1994
|
+
|
|
1995
|
+
// Tự quản lý token
|
|
1996
|
+
const tokenProvider = () => localStorage.getItem('jwt');
|
|
1997
|
+
|
|
1998
|
+
// Khởi tạo ChatHub độc lập
|
|
1999
|
+
const chatHub = new ChatHubClient({
|
|
2000
|
+
hubUrl: 'https://chat-api.example.com/hubs/chat',
|
|
2001
|
+
tokenProvider,
|
|
2002
|
+
logLevel: LogLevel.Warning, // optional
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// Khởi tạo NotificationHub độc lập
|
|
2006
|
+
const notificationHub = new NotificationHubClient({
|
|
2007
|
+
hubUrl: 'https://chat-api.example.com/hubs/notifications',
|
|
2008
|
+
tokenProvider,
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
await chatHub.connect();
|
|
2012
|
+
await notificationHub.connect();
|
|
2013
|
+
|
|
2014
|
+
// Kết hợp với ReconnectionManager
|
|
2015
|
+
import { ReconnectionManager } from 'chat-client-sdk';
|
|
2016
|
+
|
|
2017
|
+
const manager = new ReconnectionManager({
|
|
2018
|
+
chatHub,
|
|
2019
|
+
notificationHub,
|
|
2020
|
+
onTokenExpired: async () => {
|
|
2021
|
+
const newToken = await refreshMyToken();
|
|
2022
|
+
return newToken;
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
2025
|
+
manager.start();
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
> ⚠️ Khi dùng standalone, bạn phải tự quản lý token và truyền đúng `hubUrl` (bao gồm full URL kèm `/hubs/chat` hoặc `/hubs/notifications`).
|
|
2029
|
+
|
|
2030
|
+
---
|
|
2031
|
+
|
|
2032
|
+
## 18. TypedEventEmitter
|
|
2033
|
+
|
|
2034
|
+
SDK dùng `TypedEventEmitter<TEventMap>` nội bộ cho cả hai hub. Giao diện public của nó được expose qua `.on()` và `.off()`.
|
|
2035
|
+
|
|
2036
|
+
```ts
|
|
2037
|
+
// Đăng ký handler — trả về hàm unsubscribe
|
|
2038
|
+
const unsub = hub.on('eventName', (payload) => { /* ... */ });
|
|
2039
|
+
|
|
2040
|
+
// Hủy bằng hàm unsubscribe
|
|
2041
|
+
unsub();
|
|
2042
|
+
|
|
2043
|
+
// Hoặc hủy bằng .off()
|
|
2044
|
+
hub.off('eventName', handler);
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
**Lưu ý:** Khi dùng `.off()`, phải truyền chính xác **cùng một reference hàm** đã đăng ký. Dùng cách unsubscribe function từ `.on()` để tránh lỗi này.
|
|
2048
|
+
|
|
2049
|
+
> ⚠️ **`removeAllListeners()` không được expose** trên `ChatHubClient` hay `NotificationHubClient`. Method này chỉ tồn tại trên class `TypedEventEmitter` khi dùng trực tiếp — không qua hub. Hai hub client chỉ expose `.on()` và `.off()`. Để dọn dẹp nhiều listener khi unmount component, lưu lại từng unsubscribe function và gọi từng cái:
|
|
2050
|
+
|
|
2051
|
+
```ts
|
|
2052
|
+
const unsubs: Array<() => void> = [];
|
|
2053
|
+
unsubs.push(hub.on('messageReceived', handler1));
|
|
2054
|
+
unsubs.push(hub.on('typingStarted', handler2));
|
|
2055
|
+
|
|
2056
|
+
// Khi cleanup:
|
|
2057
|
+
unsubs.forEach(fn => fn());
|
|
2058
|
+
```
|
|
2059
|
+
|
|
2060
|
+
---
|
|
2061
|
+
|
|
2062
|
+
## 19. Lưu ý quan trọng
|
|
2063
|
+
|
|
2064
|
+
### Cursor pagination — KHÔNG cast sang number
|
|
2065
|
+
|
|
2066
|
+
```ts
|
|
2067
|
+
// ✅ Đúng
|
|
2068
|
+
const cursor: string = result.nextCursor!;
|
|
2069
|
+
await client.conversations.list({ cursor });
|
|
2070
|
+
|
|
2071
|
+
// ❌ Sai — mất độ chính xác vì vượt Number.MAX_SAFE_INTEGER
|
|
2072
|
+
const cursor = Number(result.nextCursor); // BUG!
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
### Token và SignalR
|
|
2076
|
+
|
|
2077
|
+
- `setToken()` hoạt động ngay cho **HTTP requests**.
|
|
2078
|
+
- SignalR đọc token khi **connect()**, không phải mid-session.
|
|
2079
|
+
- Để cập nhật token cho SignalR: `disconnect()` → `setToken()` → `connect()`.
|
|
2080
|
+
|
|
2081
|
+
### REST vs SignalR — field name khác nhau
|
|
2082
|
+
|
|
2083
|
+
| Thao tác | REST | SignalR |
|
|
2084
|
+
|----------|------|---------|
|
|
2085
|
+
| Edit message | `{ blocks, mentions }` | `{ messageId, newBlocks, newMentions }` |
|
|
2086
|
+
| Delete message | `messages.delete(messageId)` | `deleteMessage({ messageId })` — object, không phải string |
|
|
2087
|
+
| Stream completion | Không có event | `streamCompleted` thay `messageReceived` |
|
|
2088
|
+
|
|
2089
|
+
### File upload — storageKey vs URL
|
|
2090
|
+
|
|
2091
|
+
```ts
|
|
2092
|
+
// Sau khi upload, dùng storageKey làm MediaReference
|
|
2093
|
+
const { storageKey } = await client.files.uploadFile(file);
|
|
2094
|
+
|
|
2095
|
+
// Trong block:
|
|
2096
|
+
const block: ImageBlock = {
|
|
2097
|
+
$type: 'image',
|
|
2098
|
+
source: { storageKey }, // KHÔNG phải url
|
|
2099
|
+
// ...
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// Để hiển thị ảnh:
|
|
2103
|
+
const displayUrl = client.files.getDownloadUrl(storageKey);
|
|
2104
|
+
```
|
|
2105
|
+
|
|
2106
|
+
### BotDto — không có tên/avatar
|
|
2107
|
+
|
|
2108
|
+
`BotDto` **không** chứa `uniqueName` hay `fullName`. Phải gọi thêm:
|
|
2109
|
+
|
|
2110
|
+
```ts
|
|
2111
|
+
const bot = await client.bots.getById('bot-id');
|
|
2112
|
+
const participant = await client.participants.getById(bot.participantId);
|
|
2113
|
+
console.log(participant.fullName); // Tên hiển thị của bot
|
|
2114
|
+
```
|
|
2115
|
+
|
|
2116
|
+
### HealthStatus duration — là string, không phải number
|
|
2117
|
+
|
|
2118
|
+
```ts
|
|
2119
|
+
const health = await client.health.ready();
|
|
2120
|
+
// ✅ Đúng
|
|
2121
|
+
console.log(health.totalDuration); // "00:00:00.1234567"
|
|
2122
|
+
|
|
2123
|
+
// ❌ Sai
|
|
2124
|
+
const ms = Number(health.totalDuration); // NaN
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
---
|
|
2128
|
+
|
|
2129
|
+
## 20. Ví dụ đầy đủ
|
|
2130
|
+
|
|
2131
|
+
### Ví dụ 1: Chat app cơ bản
|
|
2132
|
+
|
|
2133
|
+
```ts
|
|
2134
|
+
import { ChatClient, ChatApiError } from 'chat-client-sdk';
|
|
2135
|
+
import type { ChatMessageDto } from 'chat-client-sdk';
|
|
2136
|
+
|
|
2137
|
+
// 1. Khởi tạo
|
|
2138
|
+
const client = new ChatClient({ baseUrl: 'https://chat-api.example.com' });
|
|
2139
|
+
|
|
2140
|
+
// 2. Đăng nhập
|
|
2141
|
+
await client.auth.loginWithGoogle(googleIdToken);
|
|
2142
|
+
console.log('Xin chào,', client.currentUser?.fullName);
|
|
2143
|
+
|
|
2144
|
+
// 3. Kết nối hub (thứ tự: notifications trước, chat sau)
|
|
2145
|
+
await client.realtime.notifications.connect();
|
|
2146
|
+
await client.realtime.chat.connect();
|
|
2147
|
+
|
|
2148
|
+
// 4. Lắng nghe thông báo
|
|
2149
|
+
client.realtime.notifications.on('newMessageNotification', (notification) => {
|
|
2150
|
+
console.log(`[${notification.conversationId}] ${notification.senderName}: ${notification.contentPreview}`);
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
client.realtime.notifications.on('unreadCountChanged', (dto) => {
|
|
2154
|
+
updateBadge(dto.conversationId, dto.unreadCount);
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
// 5. Mở cuộc hội thoại và chat
|
|
2158
|
+
const convId = 'conv-id-here';
|
|
2159
|
+
await client.realtime.chat.joinConversation(convId);
|
|
2160
|
+
|
|
2161
|
+
// Lắng nghe tin nhắn
|
|
2162
|
+
const unsubMsg = client.realtime.chat.on('messageReceived', (msg: ChatMessageDto) => {
|
|
2163
|
+
appendMessageToUI(msg);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
// Lắng nghe typing
|
|
2167
|
+
client.realtime.chat.on('typingStarted', (dto) => {
|
|
2168
|
+
showTypingIndicator(dto.participantName);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
// 6. Gửi tin nhắn
|
|
2172
|
+
async function sendMessage(text: string) {
|
|
2173
|
+
const ack = await client.realtime.chat.sendMessage({
|
|
2174
|
+
conversationId: convId,
|
|
2175
|
+
blocks: [{ $type: 'text', format: 'Plain', content: text, plainText: text }],
|
|
2176
|
+
});
|
|
2177
|
+
if (!ack.success) throw new Error(ack.errorMessage);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// 7. Upload và gửi file
|
|
2181
|
+
async function sendFile(file: File) {
|
|
2182
|
+
const { storageKey } = await client.files.uploadFile(file, (loaded, total) => {
|
|
2183
|
+
setProgress(Math.round((loaded / total) * 100));
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
await client.realtime.chat.sendMessage({
|
|
2187
|
+
conversationId: convId,
|
|
2188
|
+
blocks: [{
|
|
2189
|
+
$type: 'file',
|
|
2190
|
+
source: { storageKey },
|
|
2191
|
+
fileName: file.name,
|
|
2192
|
+
mimeType: file.type,
|
|
2193
|
+
fileSizeBytes: file.size,
|
|
2194
|
+
}],
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// 8. Đánh máy indicator
|
|
2199
|
+
let typingTimer: ReturnType<typeof setTimeout>;
|
|
2200
|
+
function onUserType() {
|
|
2201
|
+
client.realtime.chat.startTyping(convId);
|
|
2202
|
+
clearTimeout(typingTimer);
|
|
2203
|
+
typingTimer = setTimeout(() => client.realtime.chat.stopTyping(convId), 2000);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// 9. Dọn dẹp khi thoát
|
|
2207
|
+
async function cleanup() {
|
|
2208
|
+
unsubMsg(); // Hủy event listener
|
|
2209
|
+
await client.disconnect();
|
|
2210
|
+
}
|
|
2211
|
+
```
|
|
2212
|
+
|
|
2213
|
+
### Ví dụ 2: Load lịch sử tin nhắn với infinite scroll
|
|
2214
|
+
|
|
2215
|
+
```ts
|
|
2216
|
+
let cursor: string | null = null;
|
|
2217
|
+
let loading = false;
|
|
2218
|
+
|
|
2219
|
+
async function loadMoreMessages(conversationId: string) {
|
|
2220
|
+
if (loading) return;
|
|
2221
|
+
loading = true;
|
|
2222
|
+
|
|
2223
|
+
try {
|
|
2224
|
+
const result = await client.messages.getHistory(conversationId, {
|
|
2225
|
+
limit: 30,
|
|
2226
|
+
cursor: cursor ?? undefined,
|
|
2227
|
+
direction: 'older',
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
result.items.forEach(msg => prependMessageToUI(msg));
|
|
2231
|
+
cursor = result.nextCursor;
|
|
2232
|
+
|
|
2233
|
+
if (!result.hasMore) {
|
|
2234
|
+
hideLoadMoreButton();
|
|
2235
|
+
}
|
|
2236
|
+
} finally {
|
|
2237
|
+
loading = false;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
### Ví dụ 3: Hiển thị trạng thái online
|
|
2243
|
+
|
|
2244
|
+
```ts
|
|
2245
|
+
await client.realtime.notifications.connect();
|
|
2246
|
+
|
|
2247
|
+
// Load danh sách participant trong cuộc hội thoại
|
|
2248
|
+
const participants = await client.conversations.getParticipants('conv-id');
|
|
2249
|
+
const participantIds = participants.map(p => p.participantId);
|
|
2250
|
+
|
|
2251
|
+
// Subscribe presence
|
|
2252
|
+
await client.realtime.notifications.subscribeToPresence(participantIds);
|
|
2253
|
+
|
|
2254
|
+
// Nhận trạng thái ban đầu
|
|
2255
|
+
client.realtime.notifications.on('presenceState', (states) => {
|
|
2256
|
+
states.forEach(s => {
|
|
2257
|
+
updateOnlineIndicator(s.participantId, s.isOnline);
|
|
2258
|
+
});
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
// Cập nhật real-time
|
|
2262
|
+
client.realtime.notifications.on('presenceChanged', (dto) => {
|
|
2263
|
+
updateOnlineIndicator(dto.participantId, dto.isOnline);
|
|
2264
|
+
if (!dto.isOnline && dto.lastSeenAt) {
|
|
2265
|
+
showLastSeen(dto.participantId, new Date(dto.lastSeenAt));
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
### Ví dụ 4: Xử lý bot streaming
|
|
2271
|
+
|
|
2272
|
+
```ts
|
|
2273
|
+
const streamBuffers = new Map<string, string[]>();
|
|
2274
|
+
|
|
2275
|
+
client.realtime.chat.on('streamStarted', (dto) => {
|
|
2276
|
+
streamBuffers.set(dto.streamId, []);
|
|
2277
|
+
addStreamingPlaceholder(dto.messageId, dto.senderName);
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
client.realtime.chat.on('streamStatusUpdated', (dto) => {
|
|
2281
|
+
updateStreamStatus(dto.messageId, dto.status); // e.g. "thinking", "searching"
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
client.realtime.chat.on('streamChunkReceived', (dto) => {
|
|
2285
|
+
const chunks = streamBuffers.get(dto.streamId) ?? [];
|
|
2286
|
+
chunks.push(dto.text);
|
|
2287
|
+
streamBuffers.set(dto.streamId, chunks);
|
|
2288
|
+
updateStreamingText(dto.messageId, chunks.join(''));
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
client.realtime.chat.on('streamCompleted', (dto) => {
|
|
2292
|
+
streamBuffers.delete(dto.streamId);
|
|
2293
|
+
replaceWithFinalMessage(dto.message); // ChatMessageDto đầy đủ
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
client.realtime.chat.on('streamAborted', (dto) => {
|
|
2297
|
+
streamBuffers.delete(dto.streamId);
|
|
2298
|
+
showStreamError(dto.messageId, dto.reason);
|
|
2299
|
+
});
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
### Ví dụ 5: Xử lý lỗi và token refresh
|
|
2303
|
+
|
|
2304
|
+
```ts
|
|
2305
|
+
import { ReconnectionManager } from 'chat-client-sdk';
|
|
2306
|
+
|
|
2307
|
+
let isRefreshing = false;
|
|
2308
|
+
|
|
2309
|
+
const manager = new ReconnectionManager({
|
|
2310
|
+
chatHub: client.realtime.chat,
|
|
2311
|
+
notificationHub: client.realtime.notifications,
|
|
2312
|
+
onTokenExpired: async () => {
|
|
2313
|
+
if (isRefreshing) return null;
|
|
2314
|
+
isRefreshing = true;
|
|
2315
|
+
try {
|
|
2316
|
+
const newToken = await myAuthService.refreshToken();
|
|
2317
|
+
if (newToken) {
|
|
2318
|
+
client.setToken(newToken);
|
|
2319
|
+
localStorage.setItem('token', newToken);
|
|
2320
|
+
}
|
|
2321
|
+
return newToken;
|
|
2322
|
+
} finally {
|
|
2323
|
+
isRefreshing = false;
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
manager.start();
|
|
2329
|
+
|
|
2330
|
+
// Lắng nghe lỗi hub
|
|
2331
|
+
client.realtime.chat.on('error', (err) => {
|
|
2332
|
+
console.error(`Hub error [${err.code}]: ${err.message}`);
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
client.realtime.chat.on('disconnected', (err) => {
|
|
2336
|
+
if (err) console.warn('ChatHub disconnected:', err.message);
|
|
2337
|
+
showReconnectingUI();
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
client.realtime.chat.on('reconnected', () => {
|
|
2341
|
+
hideReconnectingUI();
|
|
2342
|
+
});
|
|
2343
|
+
```
|