@jokkoo/sdk-web 0.1.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/LICENSE +17 -0
- package/README.md +51 -0
- package/dist/index.d.mts +419 -0
- package/dist/index.d.ts +419 -0
- package/dist/index.js +1291 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1221 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var socket_ioClient = require('socket.io-client');
|
|
4
|
+
|
|
5
|
+
// src/core/debug.ts
|
|
6
|
+
var LOG_PREFIX = "[Jokkoo]";
|
|
7
|
+
function noop() {
|
|
8
|
+
}
|
|
9
|
+
function createNoopLogger() {
|
|
10
|
+
return {
|
|
11
|
+
log: noop,
|
|
12
|
+
warn: noop,
|
|
13
|
+
error: noop,
|
|
14
|
+
request: noop,
|
|
15
|
+
response: noop,
|
|
16
|
+
requestError: noop
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createDebugLogger(enabled) {
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
return createNoopLogger();
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
log(message, ...details) {
|
|
25
|
+
if (details.length > 0) {
|
|
26
|
+
console.log(`${LOG_PREFIX} ${message}`, ...details);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
30
|
+
},
|
|
31
|
+
warn(message, ...details) {
|
|
32
|
+
if (details.length > 0) {
|
|
33
|
+
console.warn(`${LOG_PREFIX} ${message}`, ...details);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.warn(`${LOG_PREFIX} ${message}`);
|
|
37
|
+
},
|
|
38
|
+
error(message, ...details) {
|
|
39
|
+
if (details.length > 0) {
|
|
40
|
+
console.error(`${LOG_PREFIX} ${message}`, ...details);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.error(`${LOG_PREFIX} ${message}`);
|
|
44
|
+
},
|
|
45
|
+
request(method, url, details) {
|
|
46
|
+
if (details) {
|
|
47
|
+
console.log(`${LOG_PREFIX} \u2192 ${method} ${url}`, details);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`${LOG_PREFIX} \u2192 ${method} ${url}`);
|
|
51
|
+
},
|
|
52
|
+
response(method, url, status, durationMs, details) {
|
|
53
|
+
const summary = `${LOG_PREFIX} \u2190 ${method} ${url} ${status} (${durationMs}ms)`;
|
|
54
|
+
if (details) {
|
|
55
|
+
console.log(summary, details);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log(summary);
|
|
59
|
+
},
|
|
60
|
+
requestError(method, url, durationMs, error) {
|
|
61
|
+
console.error(`${LOG_PREFIX} \u2715 ${method} ${url} (${durationMs}ms)`, error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function redactToken(token) {
|
|
66
|
+
if (token.length < 9) {
|
|
67
|
+
return "***";
|
|
68
|
+
}
|
|
69
|
+
return `${token.slice(0, 4)}\u2026${token.slice(-4)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/api/client.ts
|
|
73
|
+
var CLIENT_TOKEN_HEADER = "x-client-token";
|
|
74
|
+
var USER_TOKEN_HEADER = "x-user-token";
|
|
75
|
+
var JokkooApiError = class extends Error {
|
|
76
|
+
constructor(message, status, body) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.status = status;
|
|
79
|
+
this.body = body;
|
|
80
|
+
this.name = "JokkooApiError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var JokkooApiClient = class {
|
|
84
|
+
constructor(config, tokenManager) {
|
|
85
|
+
this.config = config;
|
|
86
|
+
this.tokenManager = tokenManager;
|
|
87
|
+
this.debug = createDebugLogger(config.debug);
|
|
88
|
+
}
|
|
89
|
+
resolveUrl(path) {
|
|
90
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
return `${this.config.baseUrl}${path}`;
|
|
94
|
+
}
|
|
95
|
+
async getConversations(limit, offset) {
|
|
96
|
+
return this.request(
|
|
97
|
+
"GET",
|
|
98
|
+
`/api/conversations?limit=${limit}&offset=${offset}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async getUnreadCount() {
|
|
102
|
+
return this.request("GET", "/api/conversations/unread-count");
|
|
103
|
+
}
|
|
104
|
+
async markAsRead(conversationId) {
|
|
105
|
+
await this.request("POST", `/api/conversations/${conversationId}/read`);
|
|
106
|
+
}
|
|
107
|
+
async getMessages(conversationId, limit, offset) {
|
|
108
|
+
return this.request(
|
|
109
|
+
"GET",
|
|
110
|
+
`/api/conversations/${conversationId}/messages?limit=${limit}&offset=${offset}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
async sendMessage(payload) {
|
|
114
|
+
return this.request("POST", "/api/conversations/messages", {
|
|
115
|
+
body: JSON.stringify(payload)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async requestUploadUrl(fileName, mimeType, provider = "s3") {
|
|
119
|
+
return this.request(
|
|
120
|
+
"POST",
|
|
121
|
+
"/api/storage/sdk/upload-url",
|
|
122
|
+
{
|
|
123
|
+
body: JSON.stringify({ provider, fileName, mimeType })
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
async confirmUpload(fileId) {
|
|
128
|
+
return this.request(
|
|
129
|
+
"POST",
|
|
130
|
+
`/api/storage/sdk/confirm/${fileId}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
async getDownloadUrl(fileId) {
|
|
134
|
+
return this.request(
|
|
135
|
+
"GET",
|
|
136
|
+
`/api/storage/sdk/download-url/${fileId}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
async getConversationPolicies() {
|
|
140
|
+
return this.request("GET", "/api/conversations/policies");
|
|
141
|
+
}
|
|
142
|
+
async recordSatisfaction(conversationId, score) {
|
|
143
|
+
await this.request("POST", `/api/conversations/${conversationId}/satisfaction`, {
|
|
144
|
+
body: JSON.stringify({ score })
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async request(method, path, init, isRetry = false) {
|
|
148
|
+
const userToken = await this.tokenManager.getToken();
|
|
149
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
150
|
+
const httpMethod = init?.method ?? method;
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
let requestBody;
|
|
153
|
+
if (init?.body && typeof init.body === "string") {
|
|
154
|
+
try {
|
|
155
|
+
requestBody = JSON.parse(init.body);
|
|
156
|
+
} catch {
|
|
157
|
+
requestBody = init.body;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.debug.request(httpMethod, url, {
|
|
161
|
+
clientToken: redactToken(this.config.clientToken),
|
|
162
|
+
userToken: redactToken(userToken),
|
|
163
|
+
body: requestBody
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
...init,
|
|
168
|
+
method: httpMethod,
|
|
169
|
+
headers: {
|
|
170
|
+
Accept: "application/json",
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
[CLIENT_TOKEN_HEADER]: this.config.clientToken,
|
|
173
|
+
[USER_TOKEN_HEADER]: userToken,
|
|
174
|
+
...init?.headers
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const durationMs = Date.now() - startedAt;
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
let body;
|
|
180
|
+
try {
|
|
181
|
+
body = await response.json();
|
|
182
|
+
} catch {
|
|
183
|
+
body = void 0;
|
|
184
|
+
}
|
|
185
|
+
const message = typeof body === "object" && body !== null && "message" in body && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
|
|
186
|
+
if (response.status === 401 && !isRetry) {
|
|
187
|
+
this.debug.log("Received 401. Invalidating token and retrying once.");
|
|
188
|
+
this.tokenManager.invalidateCachedToken();
|
|
189
|
+
await this.tokenManager.refreshToken();
|
|
190
|
+
return this.request(method, path, init, true);
|
|
191
|
+
}
|
|
192
|
+
const error = new JokkooApiError(message, response.status, body);
|
|
193
|
+
this.debug.requestError(httpMethod, url, durationMs, {
|
|
194
|
+
status: response.status,
|
|
195
|
+
message,
|
|
196
|
+
body
|
|
197
|
+
});
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
if (response.status === 204) {
|
|
201
|
+
this.debug.response(httpMethod, url, response.status, durationMs);
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const responseText = await response.text();
|
|
205
|
+
if (!responseText.trim()) {
|
|
206
|
+
this.debug.response(httpMethod, url, response.status, durationMs);
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
const data = JSON.parse(responseText);
|
|
210
|
+
this.debug.response(httpMethod, url, response.status, durationMs, summarizeResponse(data));
|
|
211
|
+
return data;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (!(error instanceof JokkooApiError)) {
|
|
214
|
+
const durationMs = Date.now() - startedAt;
|
|
215
|
+
this.debug.requestError(httpMethod, url, durationMs, error);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function summarizeResponse(data) {
|
|
222
|
+
if (!data || typeof data !== "object") {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
if ("data" in data && Array.isArray(data.data)) {
|
|
226
|
+
const paginated = data;
|
|
227
|
+
return {
|
|
228
|
+
itemCount: paginated.data.length,
|
|
229
|
+
total: paginated.total,
|
|
230
|
+
limit: paginated.limit,
|
|
231
|
+
offset: paginated.offset
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if ("id" in data) {
|
|
235
|
+
const entity = data;
|
|
236
|
+
return {
|
|
237
|
+
id: entity.id,
|
|
238
|
+
conversationId: entity.conversationId
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/api/types.ts
|
|
245
|
+
var NEW_CONVERSATION_ID = 0;
|
|
246
|
+
|
|
247
|
+
// src/realtime/realtime-events.ts
|
|
248
|
+
var REALTIME_EVENTS = {
|
|
249
|
+
ping: "ping",
|
|
250
|
+
pong: "pong",
|
|
251
|
+
messageCreated: "message.created",
|
|
252
|
+
conversationCreated: "conversation.created",
|
|
253
|
+
conversationUpdated: "conversation.updated"
|
|
254
|
+
};
|
|
255
|
+
var REALTIME_NAMESPACE = "/sdk";
|
|
256
|
+
|
|
257
|
+
// src/realtime/realtime-client.ts
|
|
258
|
+
var JokkooRealtimeClient = class {
|
|
259
|
+
constructor(config, tokenManager) {
|
|
260
|
+
this.config = config;
|
|
261
|
+
this.tokenManager = tokenManager;
|
|
262
|
+
this.socket = null;
|
|
263
|
+
this.status = "disconnected";
|
|
264
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
265
|
+
}
|
|
266
|
+
async connect() {
|
|
267
|
+
if (this.socket?.connected) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.disconnect();
|
|
271
|
+
const userToken = await this.tokenManager.getToken();
|
|
272
|
+
this.socket = socket_ioClient.io(`${this.config.baseUrl}${REALTIME_NAMESPACE}`, {
|
|
273
|
+
auth: {
|
|
274
|
+
clientToken: this.config.clientToken,
|
|
275
|
+
userToken
|
|
276
|
+
},
|
|
277
|
+
autoConnect: true,
|
|
278
|
+
reconnection: true,
|
|
279
|
+
reconnectionAttempts: Infinity,
|
|
280
|
+
reconnectionDelay: 1e3,
|
|
281
|
+
reconnectionDelayMax: 3e4,
|
|
282
|
+
transports: ["websocket"]
|
|
283
|
+
});
|
|
284
|
+
this.socket.on("connect", () => {
|
|
285
|
+
this.setStatus("connected");
|
|
286
|
+
});
|
|
287
|
+
this.socket.on("disconnect", () => {
|
|
288
|
+
this.setStatus("disconnected");
|
|
289
|
+
});
|
|
290
|
+
this.socket.on("connect_error", () => {
|
|
291
|
+
this.setStatus("error");
|
|
292
|
+
});
|
|
293
|
+
this.socket.io.on("reconnect_attempt", () => {
|
|
294
|
+
void this.refreshAuth();
|
|
295
|
+
this.setStatus("connecting");
|
|
296
|
+
});
|
|
297
|
+
this.setStatus("connecting");
|
|
298
|
+
}
|
|
299
|
+
disconnect() {
|
|
300
|
+
if (this.socket) {
|
|
301
|
+
this.socket.removeAllListeners();
|
|
302
|
+
this.socket.io.removeAllListeners();
|
|
303
|
+
this.socket.disconnect();
|
|
304
|
+
this.socket = null;
|
|
305
|
+
}
|
|
306
|
+
this.setStatus("disconnected");
|
|
307
|
+
}
|
|
308
|
+
getStatus() {
|
|
309
|
+
return this.status;
|
|
310
|
+
}
|
|
311
|
+
onStatusChange(listener) {
|
|
312
|
+
this.listeners.add(listener);
|
|
313
|
+
return () => {
|
|
314
|
+
this.listeners.delete(listener);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** @internal For future event subscription by feature modules */
|
|
318
|
+
getSocket() {
|
|
319
|
+
return this.socket;
|
|
320
|
+
}
|
|
321
|
+
async refreshAuth() {
|
|
322
|
+
if (!this.socket) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const freshToken = await this.tokenManager.getToken();
|
|
327
|
+
this.socket.auth = {
|
|
328
|
+
clientToken: this.config.clientToken,
|
|
329
|
+
userToken: freshToken
|
|
330
|
+
};
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
setStatus(status) {
|
|
335
|
+
this.status = status;
|
|
336
|
+
for (const listener of this.listeners) {
|
|
337
|
+
listener(status);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/core/config.ts
|
|
343
|
+
var DEFAULT_BASE_URL = "https://api.jokkoo.ai";
|
|
344
|
+
var DEFAULT_TOKEN_REFRESH_BUFFER_SECONDS = 60;
|
|
345
|
+
var DEFAULT_LOCALE = "fr";
|
|
346
|
+
function normalizeBaseUrl(baseUrl) {
|
|
347
|
+
let url = baseUrl.trim().replace(/\/+$/, "");
|
|
348
|
+
if (url.endsWith("/v1")) {
|
|
349
|
+
url = url.slice(0, -3);
|
|
350
|
+
}
|
|
351
|
+
if (url.endsWith("/api")) {
|
|
352
|
+
url = url.slice(0, -4);
|
|
353
|
+
}
|
|
354
|
+
return url;
|
|
355
|
+
}
|
|
356
|
+
var JokkooConfigError = class extends Error {
|
|
357
|
+
constructor(message) {
|
|
358
|
+
super(message);
|
|
359
|
+
this.name = "JokkooConfigError";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
function validateConfig(config) {
|
|
363
|
+
if (!config || typeof config !== "object") {
|
|
364
|
+
throw new JokkooConfigError("Jokkoo.init() requires a configuration object.");
|
|
365
|
+
}
|
|
366
|
+
if (!config.clientToken || typeof config.clientToken !== "string") {
|
|
367
|
+
throw new JokkooConfigError("clientToken is required and must be a non-empty string.");
|
|
368
|
+
}
|
|
369
|
+
if (typeof config.userTokenProvider !== "function") {
|
|
370
|
+
throw new JokkooConfigError("userTokenProvider is required and must be a function.");
|
|
371
|
+
}
|
|
372
|
+
if (config.baseUrl !== void 0 && typeof config.baseUrl !== "string") {
|
|
373
|
+
throw new JokkooConfigError("baseUrl must be a string when provided.");
|
|
374
|
+
}
|
|
375
|
+
if (config.locale !== void 0 && typeof config.locale !== "string") {
|
|
376
|
+
throw new JokkooConfigError("locale must be a string when provided.");
|
|
377
|
+
}
|
|
378
|
+
if (config.debug !== void 0 && typeof config.debug !== "boolean") {
|
|
379
|
+
throw new JokkooConfigError("debug must be a boolean when provided.");
|
|
380
|
+
}
|
|
381
|
+
if (config.tokenRefreshBufferSeconds !== void 0 && (typeof config.tokenRefreshBufferSeconds !== "number" || config.tokenRefreshBufferSeconds < 0)) {
|
|
382
|
+
throw new JokkooConfigError(
|
|
383
|
+
"tokenRefreshBufferSeconds must be a non-negative number when provided."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
clientToken: config.clientToken.trim(),
|
|
388
|
+
userTokenProvider: config.userTokenProvider,
|
|
389
|
+
baseUrl: normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE_URL),
|
|
390
|
+
locale: config.locale ?? DEFAULT_LOCALE,
|
|
391
|
+
debug: config.debug ?? false,
|
|
392
|
+
tokenRefreshBufferSeconds: config.tokenRefreshBufferSeconds ?? DEFAULT_TOKEN_REFRESH_BUFFER_SECONDS
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/core/jwt.ts
|
|
397
|
+
function base64UrlDecode(input) {
|
|
398
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
399
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
400
|
+
if (typeof globalThis.atob === "function") {
|
|
401
|
+
return globalThis.atob(padded);
|
|
402
|
+
}
|
|
403
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
404
|
+
let output = "";
|
|
405
|
+
let buffer = 0;
|
|
406
|
+
let bits = 0;
|
|
407
|
+
for (const char of padded) {
|
|
408
|
+
if (char === "=") {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
const value = chars.indexOf(char);
|
|
412
|
+
if (value === -1) {
|
|
413
|
+
throw new Error("Invalid base64 character in JWT payload.");
|
|
414
|
+
}
|
|
415
|
+
buffer = buffer << 6 | value;
|
|
416
|
+
bits += 6;
|
|
417
|
+
if (bits >= 8) {
|
|
418
|
+
bits -= 8;
|
|
419
|
+
output += String.fromCharCode(buffer >> bits & 255);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return output;
|
|
423
|
+
}
|
|
424
|
+
function decodeJwtExp(token) {
|
|
425
|
+
const parts = token.split(".");
|
|
426
|
+
if (parts.length < 2) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
431
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function isTokenExpired(token, refreshBufferSeconds, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
437
|
+
const exp = decodeJwtExp(token);
|
|
438
|
+
if (exp === null) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return exp - refreshBufferSeconds <= nowSeconds;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/core/token-manager.ts
|
|
445
|
+
var TokenManager = class {
|
|
446
|
+
constructor(config) {
|
|
447
|
+
this.config = config;
|
|
448
|
+
this.cachedToken = null;
|
|
449
|
+
this.refreshTimer = null;
|
|
450
|
+
this.inflightRefresh = null;
|
|
451
|
+
this.state = "idle";
|
|
452
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
453
|
+
this.debug = createDebugLogger(config.debug);
|
|
454
|
+
}
|
|
455
|
+
subscribe(listener) {
|
|
456
|
+
this.listeners.add(listener);
|
|
457
|
+
return () => {
|
|
458
|
+
this.listeners.delete(listener);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
getState() {
|
|
462
|
+
return this.state;
|
|
463
|
+
}
|
|
464
|
+
async getToken() {
|
|
465
|
+
if (this.cachedToken && !isTokenExpired(this.cachedToken, this.config.tokenRefreshBufferSeconds)) {
|
|
466
|
+
this.debug.log("Using cached user token.", { token: redactToken(this.cachedToken) });
|
|
467
|
+
return this.cachedToken;
|
|
468
|
+
}
|
|
469
|
+
this.debug.log("Cached user token missing or expired. Refreshing.");
|
|
470
|
+
return this.refreshToken();
|
|
471
|
+
}
|
|
472
|
+
invalidateCachedToken() {
|
|
473
|
+
this.cachedToken = null;
|
|
474
|
+
this.debug.log("Cached token invalidated.");
|
|
475
|
+
}
|
|
476
|
+
async refreshToken() {
|
|
477
|
+
if (this.inflightRefresh) {
|
|
478
|
+
return this.inflightRefresh;
|
|
479
|
+
}
|
|
480
|
+
this.setState("loading");
|
|
481
|
+
this.debug.log("Fetching user token from userTokenProvider.");
|
|
482
|
+
this.inflightRefresh = this.config.userTokenProvider().then((token) => {
|
|
483
|
+
if (!token || typeof token !== "string") {
|
|
484
|
+
throw new Error("userTokenProvider must return a non-empty string.");
|
|
485
|
+
}
|
|
486
|
+
this.cachedToken = token;
|
|
487
|
+
this.scheduleRefresh(token);
|
|
488
|
+
this.setState("ready");
|
|
489
|
+
this.debug.log("User token refreshed.", { token: redactToken(token) });
|
|
490
|
+
return token;
|
|
491
|
+
}).catch((error) => {
|
|
492
|
+
const normalized = error instanceof Error ? error : new Error("Failed to obtain user token.");
|
|
493
|
+
this.setState("error", normalized);
|
|
494
|
+
this.debug.error("Failed to refresh user token.", normalized);
|
|
495
|
+
throw normalized;
|
|
496
|
+
}).finally(() => {
|
|
497
|
+
this.inflightRefresh = null;
|
|
498
|
+
});
|
|
499
|
+
return this.inflightRefresh;
|
|
500
|
+
}
|
|
501
|
+
destroy() {
|
|
502
|
+
if (this.refreshTimer) {
|
|
503
|
+
clearTimeout(this.refreshTimer);
|
|
504
|
+
this.refreshTimer = null;
|
|
505
|
+
}
|
|
506
|
+
this.cachedToken = null;
|
|
507
|
+
this.inflightRefresh = null;
|
|
508
|
+
this.listeners.clear();
|
|
509
|
+
this.setState("idle");
|
|
510
|
+
}
|
|
511
|
+
scheduleRefresh(token) {
|
|
512
|
+
if (this.refreshTimer) {
|
|
513
|
+
clearTimeout(this.refreshTimer);
|
|
514
|
+
this.refreshTimer = null;
|
|
515
|
+
}
|
|
516
|
+
const exp = decodeJwtExp(token);
|
|
517
|
+
if (exp === null) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
521
|
+
const refreshAtSeconds = exp - this.config.tokenRefreshBufferSeconds;
|
|
522
|
+
const delayMs = Math.max((refreshAtSeconds - nowSeconds) * 1e3, 0);
|
|
523
|
+
this.debug.log("Scheduled proactive token refresh.", { delayMs });
|
|
524
|
+
this.refreshTimer = setTimeout(() => {
|
|
525
|
+
this.debug.log("Running proactive token refresh.");
|
|
526
|
+
void this.refreshToken().catch((error) => {
|
|
527
|
+
this.debug.warn("Proactive token refresh failed.", error);
|
|
528
|
+
});
|
|
529
|
+
}, delayMs);
|
|
530
|
+
}
|
|
531
|
+
setState(state, error) {
|
|
532
|
+
this.state = state;
|
|
533
|
+
for (const listener of this.listeners) {
|
|
534
|
+
listener(state, error);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/core/jokkoo.ts
|
|
540
|
+
var JokkooSDK = class {
|
|
541
|
+
constructor() {
|
|
542
|
+
this.config = null;
|
|
543
|
+
this.tokenManager = null;
|
|
544
|
+
this.realtimeClient = null;
|
|
545
|
+
this.initialized = false;
|
|
546
|
+
}
|
|
547
|
+
init(config) {
|
|
548
|
+
const debug = createDebugLogger(config.debug ?? this.config?.debug ?? false);
|
|
549
|
+
if (this.initialized) {
|
|
550
|
+
debug.warn("init() called more than once. Reinitializing SDK.");
|
|
551
|
+
this.destroy();
|
|
552
|
+
}
|
|
553
|
+
this.config = validateConfig(config);
|
|
554
|
+
this.tokenManager = new TokenManager(this.config);
|
|
555
|
+
this.realtimeClient = new JokkooRealtimeClient(this.config, this.tokenManager);
|
|
556
|
+
this.initialized = true;
|
|
557
|
+
const sdkDebug = createDebugLogger(this.config.debug);
|
|
558
|
+
sdkDebug.log("SDK initialized.", {
|
|
559
|
+
baseUrl: this.config.baseUrl,
|
|
560
|
+
locale: this.config.locale,
|
|
561
|
+
tokenRefreshBufferSeconds: this.config.tokenRefreshBufferSeconds
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
destroy() {
|
|
565
|
+
const debug = createDebugLogger(this.config?.debug ?? false);
|
|
566
|
+
this.realtimeClient?.disconnect();
|
|
567
|
+
this.realtimeClient = null;
|
|
568
|
+
this.tokenManager?.destroy();
|
|
569
|
+
this.tokenManager = null;
|
|
570
|
+
this.config = null;
|
|
571
|
+
this.initialized = false;
|
|
572
|
+
debug.log("SDK destroyed.");
|
|
573
|
+
}
|
|
574
|
+
isInitialized() {
|
|
575
|
+
return this.initialized;
|
|
576
|
+
}
|
|
577
|
+
getState() {
|
|
578
|
+
return {
|
|
579
|
+
initialized: this.initialized,
|
|
580
|
+
clientToken: this.config?.clientToken,
|
|
581
|
+
baseUrl: this.config?.baseUrl,
|
|
582
|
+
locale: this.config?.locale
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** @internal Used by UI and future messaging modules */
|
|
586
|
+
getTokenManager() {
|
|
587
|
+
this.assertInitialized();
|
|
588
|
+
return this.tokenManager;
|
|
589
|
+
}
|
|
590
|
+
/** @internal Used by UI and future messaging modules */
|
|
591
|
+
getRealtimeClient() {
|
|
592
|
+
this.assertInitialized();
|
|
593
|
+
return this.realtimeClient;
|
|
594
|
+
}
|
|
595
|
+
/** @internal Used by UI and future messaging modules */
|
|
596
|
+
getConfig() {
|
|
597
|
+
this.assertInitialized();
|
|
598
|
+
return this.config;
|
|
599
|
+
}
|
|
600
|
+
assertInitialized() {
|
|
601
|
+
if (!this.initialized || !this.config || !this.tokenManager) {
|
|
602
|
+
throw new Error("Jokkoo SDK is not initialized. Call Jokkoo.init() first.");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var Jokkoo = new JokkooSDK();
|
|
607
|
+
|
|
608
|
+
// src/types/chat-message.ts
|
|
609
|
+
function mapApiAttachmentToChatAttachment(attachment) {
|
|
610
|
+
return {
|
|
611
|
+
fileId: attachment.fileId,
|
|
612
|
+
fileName: attachment.fileName,
|
|
613
|
+
mimeType: attachment.mimeType,
|
|
614
|
+
size: attachment.size
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/i18n/en.ts
|
|
619
|
+
var en = {
|
|
620
|
+
supportCenter: "Support Center",
|
|
621
|
+
activeConversation: (count) => `${count} active ticket${count === 1 ? "" : "s"}`,
|
|
622
|
+
myRequests: "My Requests",
|
|
623
|
+
newSupportRequest: "New Support Request",
|
|
624
|
+
noConversationsYet: "No conversations yet.",
|
|
625
|
+
unableToLoadConversations: "Unable to load conversations.",
|
|
626
|
+
retry: "Retry",
|
|
627
|
+
newRequest: "New request",
|
|
628
|
+
support: "Support",
|
|
629
|
+
noMessagesYet: "No messages yet",
|
|
630
|
+
newSupportRequestHeader: "New support request",
|
|
631
|
+
request: "Request",
|
|
632
|
+
typeYourMessage: "Type your message...",
|
|
633
|
+
sendMessage: "Send message",
|
|
634
|
+
recordVoiceMessage: "Record voice message",
|
|
635
|
+
unableToLoadMessages: "Unable to load messages.",
|
|
636
|
+
discussionEnded: "This discussion has been ended",
|
|
637
|
+
discussionClosedCanReply: "This discussion is closed. You can reply to reopen it.",
|
|
638
|
+
rateConversation: "Rate your conversation",
|
|
639
|
+
rateConversationDescription: "How satisfied are you with our support?",
|
|
640
|
+
thankYouForRating: "Thank you for your feedback!",
|
|
641
|
+
ratingVeryUnsatisfied: "Very unsatisfied",
|
|
642
|
+
ratingUnsatisfied: "Unsatisfied",
|
|
643
|
+
ratingNeutral: "Neutral",
|
|
644
|
+
ratingSatisfied: "Satisfied",
|
|
645
|
+
ratingVerySatisfied: "Very satisfied",
|
|
646
|
+
sendMessageError: "Unable to send your message. Please try again.",
|
|
647
|
+
statusNew: "New",
|
|
648
|
+
statusInProgress: "In Progress",
|
|
649
|
+
statusResolved: "Resolved",
|
|
650
|
+
statusEnded: "Ended",
|
|
651
|
+
ticketResolved: "This ticket has been resolved",
|
|
652
|
+
supportAgent: "Support Agent",
|
|
653
|
+
welcomeMessage: "Hello, How can we help you ?",
|
|
654
|
+
now: "now",
|
|
655
|
+
newMessageIndicator: (count) => count === 1 ? "1 new message" : `${count} new messages`,
|
|
656
|
+
attachFile: "Attach file",
|
|
657
|
+
pickPhotoLibrary: "Photo library",
|
|
658
|
+
pickCamera: "Camera",
|
|
659
|
+
pickDocument: "Document",
|
|
660
|
+
cancel: "Cancel",
|
|
661
|
+
uploadFailed: "Upload failed",
|
|
662
|
+
maxAttachmentsReached: "You can attach up to 5 files per message",
|
|
663
|
+
sharedFiles: "Shared files",
|
|
664
|
+
uploadingAttachment: "Uploading",
|
|
665
|
+
attachmentReady: "Ready to send",
|
|
666
|
+
retryUpload: "Retry",
|
|
667
|
+
openExternally: "Open externally",
|
|
668
|
+
downloadAttachment: "Download",
|
|
669
|
+
previewUnavailableInApp: "This file cannot be previewed in the app. Use the options below to open or download it.",
|
|
670
|
+
previewLoadFailed: "Unable to load this attachment.",
|
|
671
|
+
previewImageFile: "Image",
|
|
672
|
+
previewVideoFile: "Video",
|
|
673
|
+
previewDocumentFile: "Document",
|
|
674
|
+
voiceMessage: "Voice message",
|
|
675
|
+
recording: "Recording...",
|
|
676
|
+
tapToRecord: "Tap to record",
|
|
677
|
+
cancelRecording: "Cancel",
|
|
678
|
+
sendRecording: "Send recording",
|
|
679
|
+
microphonePermissionDenied: "Microphone access is required to record voice messages.",
|
|
680
|
+
voiceMessageTooShort: "Hold to record a longer voice message."
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/i18n/fr.ts
|
|
684
|
+
var fr = {
|
|
685
|
+
supportCenter: "Centre d'assistance",
|
|
686
|
+
activeConversation: (count) => `${count} ticket${count > 1 ? "s" : ""} actif${count > 1 ? "s" : ""}`,
|
|
687
|
+
myRequests: "Mes demandes",
|
|
688
|
+
newSupportRequest: "Nouvelle demande",
|
|
689
|
+
noConversationsYet: "Aucune conversation pour le moment.",
|
|
690
|
+
unableToLoadConversations: "Impossible de charger les conversations.",
|
|
691
|
+
retry: "R\xE9essayer",
|
|
692
|
+
newRequest: "Nouvelle demande",
|
|
693
|
+
support: "Support",
|
|
694
|
+
noMessagesYet: "Aucun message pour le moment",
|
|
695
|
+
newSupportRequestHeader: "Nouvelle demande",
|
|
696
|
+
request: "Demande",
|
|
697
|
+
typeYourMessage: "Tapez votre message...",
|
|
698
|
+
sendMessage: "Envoyer le message",
|
|
699
|
+
recordVoiceMessage: "Enregistrer un message vocal",
|
|
700
|
+
unableToLoadMessages: "Impossible de charger les messages.",
|
|
701
|
+
discussionEnded: "Cette discussion est termin\xE9e",
|
|
702
|
+
discussionClosedCanReply: "Cette discussion est cl\xF4tur\xE9e. Vous pouvez r\xE9pondre pour la rouvrir.",
|
|
703
|
+
rateConversation: "Notez votre conversation",
|
|
704
|
+
rateConversationDescription: "Quelle est votre satisfaction concernant notre support ?",
|
|
705
|
+
thankYouForRating: "Merci pour votre retour !",
|
|
706
|
+
ratingVeryUnsatisfied: "Tr\xE8s insatisfait",
|
|
707
|
+
ratingUnsatisfied: "Insatisfait",
|
|
708
|
+
ratingNeutral: "Neutre",
|
|
709
|
+
ratingSatisfied: "Satisfait",
|
|
710
|
+
ratingVerySatisfied: "Tr\xE8s satisfait",
|
|
711
|
+
sendMessageError: "Impossible d'envoyer votre message. Veuillez r\xE9essayer.",
|
|
712
|
+
statusNew: "Nouveau",
|
|
713
|
+
statusInProgress: "En cours",
|
|
714
|
+
statusResolved: "R\xE9solu",
|
|
715
|
+
statusEnded: "Termin\xE9",
|
|
716
|
+
ticketResolved: "Ce ticket a \xE9t\xE9 r\xE9solu",
|
|
717
|
+
supportAgent: "Agent de support",
|
|
718
|
+
welcomeMessage: "Bonjour, comment pouvons-nous vous aider ?",
|
|
719
|
+
now: "maintenant",
|
|
720
|
+
newMessageIndicator: (count) => count === 1 ? "1 nouveau message" : `${count} nouveaux messages`,
|
|
721
|
+
attachFile: "Joindre un fichier",
|
|
722
|
+
pickPhotoLibrary: "Phototh\xE8que",
|
|
723
|
+
pickCamera: "Appareil photo",
|
|
724
|
+
pickDocument: "Document",
|
|
725
|
+
cancel: "Annuler",
|
|
726
|
+
uploadFailed: "\xC9chec du t\xE9l\xE9versement",
|
|
727
|
+
maxAttachmentsReached: "Vous pouvez joindre jusqu'\xE0 5 fichiers par message",
|
|
728
|
+
sharedFiles: "Fichiers partag\xE9s",
|
|
729
|
+
uploadingAttachment: "T\xE9l\xE9versement",
|
|
730
|
+
attachmentReady: "Pr\xEAt \xE0 envoyer",
|
|
731
|
+
retryUpload: "R\xE9essayer",
|
|
732
|
+
openExternally: "Ouvrir en externe",
|
|
733
|
+
downloadAttachment: "T\xE9l\xE9charger",
|
|
734
|
+
previewUnavailableInApp: "Ce fichier ne peut pas \xEAtre pr\xE9visualis\xE9 dans l'application. Utilisez les options ci-dessous pour l'ouvrir ou le t\xE9l\xE9charger.",
|
|
735
|
+
previewLoadFailed: "Impossible de charger cette pi\xE8ce jointe.",
|
|
736
|
+
previewImageFile: "Image",
|
|
737
|
+
previewVideoFile: "Vid\xE9o",
|
|
738
|
+
previewDocumentFile: "Document",
|
|
739
|
+
voiceMessage: "Message vocal",
|
|
740
|
+
recording: "Enregistrement...",
|
|
741
|
+
tapToRecord: "Appuyez pour enregistrer",
|
|
742
|
+
cancelRecording: "Annuler",
|
|
743
|
+
sendRecording: "Envoyer l'enregistrement",
|
|
744
|
+
microphonePermissionDenied: "L'acc\xE8s au microphone est requis pour enregistrer des messages vocaux.",
|
|
745
|
+
voiceMessageTooShort: "Maintenez plus longtemps pour enregistrer un message vocal."
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/i18n/index.ts
|
|
749
|
+
function getTranslations(locale) {
|
|
750
|
+
if (locale === "en") {
|
|
751
|
+
return en;
|
|
752
|
+
}
|
|
753
|
+
return fr;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/lib/satisfaction.ts
|
|
757
|
+
function isSatisfactionRequestMessage(message) {
|
|
758
|
+
return message.messageType === "activity" && message.metadata?.activityType === "satisfaction_request";
|
|
759
|
+
}
|
|
760
|
+
function getSatisfactionSurveyMessage(messages) {
|
|
761
|
+
const surveyMessage = [...messages].reverse().find(isSatisfactionRequestMessage);
|
|
762
|
+
const content = surveyMessage?.content?.trim();
|
|
763
|
+
return content && content.length > 0 ? content : void 0;
|
|
764
|
+
}
|
|
765
|
+
function conversationNeedsRating(status, messages) {
|
|
766
|
+
if (status === "closed") {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
if (status === "pending_closure") {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
return messages.some(isSatisfactionRequestMessage);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/lib/activity.ts
|
|
776
|
+
var TICKET_ACTIVITY_TYPES = /* @__PURE__ */ new Set(["ticket_created", "ticket_status_changed"]);
|
|
777
|
+
function isTicketActivityMessage(message) {
|
|
778
|
+
const activityType = message.metadata?.activityType;
|
|
779
|
+
return message.messageType === "activity" && typeof activityType === "string" && TICKET_ACTIVITY_TYPES.has(activityType);
|
|
780
|
+
}
|
|
781
|
+
function isPassiveActivityMessage(message) {
|
|
782
|
+
if (isSatisfactionRequestMessage(message)) {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return isTicketActivityMessage(message) || message.senderType === "system";
|
|
786
|
+
}
|
|
787
|
+
function shouldCountMessageAsUnread(message) {
|
|
788
|
+
return message.senderType === "agent" || message.senderType === "system";
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/lib/attachment-media.ts
|
|
792
|
+
function isImageMimeType(mimeType) {
|
|
793
|
+
return mimeType.startsWith("image/");
|
|
794
|
+
}
|
|
795
|
+
function isVideoMimeType(mimeType) {
|
|
796
|
+
return mimeType.startsWith("video/");
|
|
797
|
+
}
|
|
798
|
+
function isAudioMimeType(mimeType) {
|
|
799
|
+
return mimeType.startsWith("audio/");
|
|
800
|
+
}
|
|
801
|
+
function isDocumentMimeType(mimeType) {
|
|
802
|
+
return !isImageMimeType(mimeType) && !isVideoMimeType(mimeType) && !isAudioMimeType(mimeType);
|
|
803
|
+
}
|
|
804
|
+
function canPreviewInApp(mimeType) {
|
|
805
|
+
return isImageMimeType(mimeType);
|
|
806
|
+
}
|
|
807
|
+
function formatAttachmentSize(size) {
|
|
808
|
+
if (!size || size <= 0) {
|
|
809
|
+
return "";
|
|
810
|
+
}
|
|
811
|
+
if (size < 1024) {
|
|
812
|
+
return `${size} B`;
|
|
813
|
+
}
|
|
814
|
+
if (size < 1024 * 1024) {
|
|
815
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
816
|
+
}
|
|
817
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
818
|
+
}
|
|
819
|
+
function formatAudioDuration(durationMs) {
|
|
820
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
|
|
821
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
822
|
+
const seconds = totalSeconds % 60;
|
|
823
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
824
|
+
}
|
|
825
|
+
function isVoiceMessageMetadata(metadata) {
|
|
826
|
+
return metadata?.isVoiceMessage === true;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/lib/closed-conversation-policy.ts
|
|
830
|
+
function isWithinReopenGracePeriod(conversation, policy) {
|
|
831
|
+
if (!conversation.closedAt || policy.reopenGracePeriodDays <= 0) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const graceEnd = new Date(conversation.closedAt);
|
|
835
|
+
graceEnd.setDate(graceEnd.getDate() + policy.reopenGracePeriodDays);
|
|
836
|
+
return /* @__PURE__ */ new Date() <= graceEnd;
|
|
837
|
+
}
|
|
838
|
+
function canReplyToClosedConversation(conversation, policy) {
|
|
839
|
+
if (conversation.status !== "closed" || !policy) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
if (policy.endUserReplyBehavior !== "reopen") {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
return isWithinReopenGracePeriod(conversation, policy);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/lib/constants.ts
|
|
849
|
+
var MESSAGE_CONTENT_MIN_LENGTH = 2;
|
|
850
|
+
var MESSAGE_CONTENT_MAX_LENGTH = 2e3;
|
|
851
|
+
var MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
852
|
+
var MAX_VIDEO_SIZE_BYTES = 25 * 1024 * 1024;
|
|
853
|
+
var MAX_DOCUMENT_SIZE_BYTES = 10 * 1024 * 1024;
|
|
854
|
+
var MAX_AUDIO_SIZE_BYTES = 25 * 1024 * 1024;
|
|
855
|
+
var MAX_ATTACHMENTS_PER_MESSAGE = 5;
|
|
856
|
+
var ALLOWED_IMAGE_MIME_TYPES = [
|
|
857
|
+
"image/jpeg",
|
|
858
|
+
"image/png",
|
|
859
|
+
"image/gif",
|
|
860
|
+
"image/webp"
|
|
861
|
+
];
|
|
862
|
+
var ALLOWED_VIDEO_MIME_TYPES = ["video/mp4", "video/quicktime", "video/webm"];
|
|
863
|
+
var ALLOWED_DOCUMENT_MIME_TYPES = [
|
|
864
|
+
"application/pdf",
|
|
865
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
866
|
+
];
|
|
867
|
+
var ALLOWED_AUDIO_MIME_TYPES = [
|
|
868
|
+
"audio/aac",
|
|
869
|
+
"audio/mp4",
|
|
870
|
+
"audio/mpeg",
|
|
871
|
+
"audio/webm"
|
|
872
|
+
];
|
|
873
|
+
var ALLOWED_ATTACHMENT_MIME_TYPES = [
|
|
874
|
+
...ALLOWED_IMAGE_MIME_TYPES,
|
|
875
|
+
...ALLOWED_VIDEO_MIME_TYPES,
|
|
876
|
+
...ALLOWED_DOCUMENT_MIME_TYPES,
|
|
877
|
+
...ALLOWED_AUDIO_MIME_TYPES
|
|
878
|
+
];
|
|
879
|
+
var VOICE_MESSAGE_MIME_TYPE = "audio/mp4";
|
|
880
|
+
var VOICE_MESSAGE_STORAGE_PROVIDER = "s3";
|
|
881
|
+
|
|
882
|
+
// src/lib/conversation-display.ts
|
|
883
|
+
function getInitialsFromName(name) {
|
|
884
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
885
|
+
if (parts.length === 0) {
|
|
886
|
+
return "?";
|
|
887
|
+
}
|
|
888
|
+
if (parts.length === 1) {
|
|
889
|
+
return parts[0].slice(0, 2).toUpperCase();
|
|
890
|
+
}
|
|
891
|
+
return `${parts[0][0] ?? ""}${parts[parts.length - 1][0] ?? ""}`.toUpperCase();
|
|
892
|
+
}
|
|
893
|
+
function getConversationDisplayName(conversation, t) {
|
|
894
|
+
if (conversation.status === "pending") {
|
|
895
|
+
return t.newRequest;
|
|
896
|
+
}
|
|
897
|
+
if (conversation.assignedAgentName) {
|
|
898
|
+
return conversation.assignedAgentName;
|
|
899
|
+
}
|
|
900
|
+
return t.support;
|
|
901
|
+
}
|
|
902
|
+
function getAgentInitials(assignedAgentName, fallback) {
|
|
903
|
+
if (!assignedAgentName) {
|
|
904
|
+
return fallback;
|
|
905
|
+
}
|
|
906
|
+
return getInitialsFromName(assignedAgentName);
|
|
907
|
+
}
|
|
908
|
+
function getAgentDisplayName(assignedAgentName, t) {
|
|
909
|
+
return assignedAgentName ?? t.supportAgent;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/lib/file-upload.ts
|
|
913
|
+
function getMaxSizeForMimeType(mimeType) {
|
|
914
|
+
if (mimeType.startsWith("image/")) {
|
|
915
|
+
return MAX_IMAGE_SIZE_BYTES;
|
|
916
|
+
}
|
|
917
|
+
if (mimeType.startsWith("video/")) {
|
|
918
|
+
return MAX_VIDEO_SIZE_BYTES;
|
|
919
|
+
}
|
|
920
|
+
if (mimeType.startsWith("audio/")) {
|
|
921
|
+
return MAX_AUDIO_SIZE_BYTES;
|
|
922
|
+
}
|
|
923
|
+
return MAX_DOCUMENT_SIZE_BYTES;
|
|
924
|
+
}
|
|
925
|
+
function validateAttachment(mimeType, size) {
|
|
926
|
+
if (!ALLOWED_ATTACHMENT_MIME_TYPES.includes(mimeType)) {
|
|
927
|
+
return "Unsupported file type";
|
|
928
|
+
}
|
|
929
|
+
if (size > getMaxSizeForMimeType(mimeType)) {
|
|
930
|
+
return "File exceeds the maximum allowed size";
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
async function uploadAttachmentFile(apiClient, attachment, onProgress) {
|
|
935
|
+
onProgress(10, "uploading");
|
|
936
|
+
const { fileId, uploadUrl } = await apiClient.requestUploadUrl(
|
|
937
|
+
attachment.fileName,
|
|
938
|
+
attachment.mimeType
|
|
939
|
+
);
|
|
940
|
+
onProgress(40, "uploading");
|
|
941
|
+
const fileResponse = await fetch(attachment.uri);
|
|
942
|
+
const blob = await fileResponse.blob();
|
|
943
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
944
|
+
method: "PUT",
|
|
945
|
+
body: blob,
|
|
946
|
+
headers: {
|
|
947
|
+
"Content-Type": attachment.mimeType
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
if (!uploadResponse.ok) {
|
|
951
|
+
throw new Error("Upload failed");
|
|
952
|
+
}
|
|
953
|
+
onProgress(75, "confirming");
|
|
954
|
+
await apiClient.confirmUpload(fileId);
|
|
955
|
+
return {
|
|
956
|
+
...attachment,
|
|
957
|
+
status: "confirmed",
|
|
958
|
+
progress: 100,
|
|
959
|
+
confirmedFileId: fileId
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function createPendingAttachment(input) {
|
|
963
|
+
const validationError = validateAttachment(input.mimeType, input.size);
|
|
964
|
+
return {
|
|
965
|
+
id: `${input.fileName}-${Date.now()}`,
|
|
966
|
+
fileName: input.fileName,
|
|
967
|
+
mimeType: input.mimeType,
|
|
968
|
+
uri: input.uri,
|
|
969
|
+
size: input.size,
|
|
970
|
+
status: validationError ? "error" : "pending",
|
|
971
|
+
progress: 0,
|
|
972
|
+
error: validationError ?? void 0
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function canAddMoreAttachments(currentCount) {
|
|
976
|
+
return currentCount < MAX_ATTACHMENTS_PER_MESSAGE;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/lib/format.ts
|
|
980
|
+
var MONTH_LABELS = {
|
|
981
|
+
en: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
|
982
|
+
fr: ["Jan", "F\xE9v", "Mar", "Avr", "Mai", "Juin", "Juil", "Ao\xFB", "Sep", "Oct", "Nov", "D\xE9c"]
|
|
983
|
+
};
|
|
984
|
+
function resolveLocale(locale) {
|
|
985
|
+
return locale === "en" ? "en" : "fr";
|
|
986
|
+
}
|
|
987
|
+
function formatMessageTimestamp(value, locale) {
|
|
988
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
989
|
+
const monthLabels = MONTH_LABELS[resolveLocale(locale)];
|
|
990
|
+
const month = monthLabels[date.getMonth()];
|
|
991
|
+
const day = date.getDate();
|
|
992
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
993
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
994
|
+
return `${month} ${day} \xB7 ${hours}:${minutes}`;
|
|
995
|
+
}
|
|
996
|
+
function formatRelativeTimestamp(value, options) {
|
|
997
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
998
|
+
const now = Date.now();
|
|
999
|
+
const diffMs = now - date.getTime();
|
|
1000
|
+
const diffMinutes = Math.floor(diffMs / 6e4);
|
|
1001
|
+
const nowLabel = options?.nowLabel ?? "now";
|
|
1002
|
+
if (diffMinutes < 1) {
|
|
1003
|
+
return nowLabel;
|
|
1004
|
+
}
|
|
1005
|
+
if (diffMinutes < 60) {
|
|
1006
|
+
return `${diffMinutes}min`;
|
|
1007
|
+
}
|
|
1008
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
1009
|
+
if (diffHours < 24) {
|
|
1010
|
+
return `${diffHours}h`;
|
|
1011
|
+
}
|
|
1012
|
+
const monthLabels = MONTH_LABELS[resolveLocale(options?.locale)];
|
|
1013
|
+
const month = monthLabels[date.getMonth()];
|
|
1014
|
+
const day = date.getDate();
|
|
1015
|
+
return `${month} ${day}`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/lib/message-mapper.ts
|
|
1019
|
+
function mapMessageToChatMessage(message, translations, locale, assignedAgentName, assignedAgentAvatarUrl) {
|
|
1020
|
+
const sender = message.senderType === "end_user" ? "user" : message.senderType === "agent" ? "agent" : "system";
|
|
1021
|
+
const agentName = sender === "agent" ? getAgentDisplayName(assignedAgentName, translations) : void 0;
|
|
1022
|
+
const agentInitials = sender === "agent" ? getAgentInitials(assignedAgentName, "SA") : void 0;
|
|
1023
|
+
const agentAvatarUrl = sender === "agent" ? assignedAgentAvatarUrl ?? null : null;
|
|
1024
|
+
return {
|
|
1025
|
+
id: String(message.id),
|
|
1026
|
+
sender,
|
|
1027
|
+
text: message.content,
|
|
1028
|
+
timestamp: formatMessageTimestamp(message.createdAt, locale),
|
|
1029
|
+
agentName,
|
|
1030
|
+
agentInitials,
|
|
1031
|
+
agentAvatarUrl,
|
|
1032
|
+
attachments: message.attachments?.map((attachment) => ({
|
|
1033
|
+
fileId: attachment.fileId,
|
|
1034
|
+
fileName: attachment.fileName,
|
|
1035
|
+
mimeType: attachment.mimeType,
|
|
1036
|
+
size: attachment.size
|
|
1037
|
+
})),
|
|
1038
|
+
metadata: message.metadata ?? null
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function createWelcomeMessage(translations) {
|
|
1042
|
+
return {
|
|
1043
|
+
id: "welcome",
|
|
1044
|
+
sender: "agent",
|
|
1045
|
+
agentInitials: "AB",
|
|
1046
|
+
text: translations.welcomeMessage,
|
|
1047
|
+
timestamp: ""
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/lib/message-pagination.ts
|
|
1052
|
+
async function fetchMessagesPage(apiClient, conversationId, pageSize, pageParam) {
|
|
1053
|
+
if (pageParam === "latest") {
|
|
1054
|
+
const probe = await apiClient.getMessages(conversationId, 1, 0);
|
|
1055
|
+
const offset = Math.max(0, probe.total - pageSize);
|
|
1056
|
+
const page2 = await apiClient.getMessages(conversationId, pageSize, offset);
|
|
1057
|
+
return {
|
|
1058
|
+
...page2,
|
|
1059
|
+
data: [...page2.data].reverse()
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const page = await apiClient.getMessages(conversationId, pageSize, pageParam);
|
|
1063
|
+
return {
|
|
1064
|
+
...page,
|
|
1065
|
+
data: [...page.data].reverse()
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function getOlderMessagesPageParam(lastPage) {
|
|
1069
|
+
const oldestLoadedOffset = lastPage.offset;
|
|
1070
|
+
if (oldestLoadedOffset <= 0) {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
}
|
|
1073
|
+
return Math.max(0, oldestLoadedOffset - lastPage.limit);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/lib/realtime-mappers.ts
|
|
1077
|
+
function toIsoString(value) {
|
|
1078
|
+
if (!value) {
|
|
1079
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1080
|
+
}
|
|
1081
|
+
return typeof value === "string" ? value : value.toISOString();
|
|
1082
|
+
}
|
|
1083
|
+
function mapRealtimeStatus(status) {
|
|
1084
|
+
if (status === "closed") {
|
|
1085
|
+
return "closed";
|
|
1086
|
+
}
|
|
1087
|
+
if (status === "pending_closure") {
|
|
1088
|
+
return "pending_closure";
|
|
1089
|
+
}
|
|
1090
|
+
if (status === "snoozed") {
|
|
1091
|
+
return "snoozed";
|
|
1092
|
+
}
|
|
1093
|
+
if (status === "pending") {
|
|
1094
|
+
return "pending";
|
|
1095
|
+
}
|
|
1096
|
+
return "open";
|
|
1097
|
+
}
|
|
1098
|
+
function mapRealtimeConversationPayload(conversation) {
|
|
1099
|
+
return {
|
|
1100
|
+
id: conversation.id,
|
|
1101
|
+
channelType: conversation.channelType,
|
|
1102
|
+
status: mapRealtimeStatus(conversation.status),
|
|
1103
|
+
lastMessageText: conversation.lastMessageText,
|
|
1104
|
+
assignedAgentName: conversation.assignedAgentName ?? null,
|
|
1105
|
+
createdAt: toIsoString(conversation.createdAt),
|
|
1106
|
+
updatedAt: toIsoString(conversation.lastMessageAt ?? conversation.updatedAt)
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function normalizeRealtimeMessage(message) {
|
|
1110
|
+
return {
|
|
1111
|
+
...message,
|
|
1112
|
+
id: Number(message.id),
|
|
1113
|
+
conversationId: Number(message.conversationId),
|
|
1114
|
+
endUserId: Number(message.endUserId),
|
|
1115
|
+
createdAt: typeof message.createdAt === "string" ? message.createdAt : new Date(message.createdAt).toISOString()
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/lib/voice-recording.ts
|
|
1120
|
+
var VOICE_WAVEFORM_BAR_COUNT = 32;
|
|
1121
|
+
var VOICE_METERING_IDLE_LEVEL = 0.14;
|
|
1122
|
+
var VOICE_METERING_SMOOTHING = 0.38;
|
|
1123
|
+
function normalizeMeteringLevel(metering) {
|
|
1124
|
+
const clampedDb = Math.max(-58, Math.min(0, metering));
|
|
1125
|
+
const linear = (clampedDb + 58) / 58;
|
|
1126
|
+
const curved = Math.pow(linear, 0.72);
|
|
1127
|
+
return Math.max(VOICE_METERING_IDLE_LEVEL, Math.min(1, curved));
|
|
1128
|
+
}
|
|
1129
|
+
function smoothMeteringSample(previous, next, alpha = VOICE_METERING_SMOOTHING) {
|
|
1130
|
+
return previous + (next - previous) * alpha;
|
|
1131
|
+
}
|
|
1132
|
+
function createIdleMeteringLevels(barCount = VOICE_WAVEFORM_BAR_COUNT) {
|
|
1133
|
+
return Array.from({ length: barCount }, () => VOICE_METERING_IDLE_LEVEL);
|
|
1134
|
+
}
|
|
1135
|
+
function shiftMeteringLevel(levels, newLevel) {
|
|
1136
|
+
if (levels.length === 0) {
|
|
1137
|
+
return createIdleMeteringLevels();
|
|
1138
|
+
}
|
|
1139
|
+
if (levels.length === 1) {
|
|
1140
|
+
return [levels[0], newLevel];
|
|
1141
|
+
}
|
|
1142
|
+
return [...levels.slice(1), newLevel];
|
|
1143
|
+
}
|
|
1144
|
+
function applyLiveBarVariation(level, index) {
|
|
1145
|
+
const wobble = 0.82 + 0.18 * Math.sin(index * 0.85 + level * 4.2);
|
|
1146
|
+
return Math.max(VOICE_METERING_IDLE_LEVEL, Math.min(1, level * wobble));
|
|
1147
|
+
}
|
|
1148
|
+
function createPlaybackBarHeights(count, seed) {
|
|
1149
|
+
const heights = [];
|
|
1150
|
+
for (let index = 0; index < count; index += 1) {
|
|
1151
|
+
const wave = Math.sin((index + seed) * 0.65) * 0.35 + 0.55;
|
|
1152
|
+
heights.push(Math.max(0.2, Math.min(1, wave)));
|
|
1153
|
+
}
|
|
1154
|
+
return heights;
|
|
1155
|
+
}
|
|
1156
|
+
function blendLiveWaveformLevels(levels) {
|
|
1157
|
+
if (levels.length <= 2) {
|
|
1158
|
+
return levels;
|
|
1159
|
+
}
|
|
1160
|
+
return levels.map((level, index) => {
|
|
1161
|
+
const previous = levels[index - 1] ?? level;
|
|
1162
|
+
const next = levels[index + 1] ?? level;
|
|
1163
|
+
const blended = level * 0.55 + previous * 0.225 + next * 0.225;
|
|
1164
|
+
return Math.max(VOICE_METERING_IDLE_LEVEL, Math.min(1, blended));
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/lib/voice-upload.ts
|
|
1169
|
+
async function uploadVoiceRecording(apiClient, uri, fileName, onProgress) {
|
|
1170
|
+
onProgress?.(10);
|
|
1171
|
+
const { fileId, uploadUrl } = await apiClient.requestUploadUrl(
|
|
1172
|
+
fileName,
|
|
1173
|
+
VOICE_MESSAGE_MIME_TYPE,
|
|
1174
|
+
VOICE_MESSAGE_STORAGE_PROVIDER
|
|
1175
|
+
);
|
|
1176
|
+
onProgress?.(40);
|
|
1177
|
+
const fileResponse = await fetch(uri);
|
|
1178
|
+
const blob = await fileResponse.blob();
|
|
1179
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
1180
|
+
method: "PUT",
|
|
1181
|
+
body: blob,
|
|
1182
|
+
headers: {
|
|
1183
|
+
"Content-Type": VOICE_MESSAGE_MIME_TYPE
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
if (!uploadResponse.ok) {
|
|
1187
|
+
throw new Error("Voice upload failed");
|
|
1188
|
+
}
|
|
1189
|
+
onProgress?.(75);
|
|
1190
|
+
await apiClient.confirmUpload(fileId);
|
|
1191
|
+
onProgress?.(100);
|
|
1192
|
+
return fileId;
|
|
1193
|
+
}
|
|
1194
|
+
async function uploadVoiceBlob(apiClient, blob, fileName, mimeType = VOICE_MESSAGE_MIME_TYPE, onProgress) {
|
|
1195
|
+
onProgress?.(10);
|
|
1196
|
+
const { fileId, uploadUrl } = await apiClient.requestUploadUrl(
|
|
1197
|
+
fileName,
|
|
1198
|
+
mimeType,
|
|
1199
|
+
VOICE_MESSAGE_STORAGE_PROVIDER
|
|
1200
|
+
);
|
|
1201
|
+
onProgress?.(40);
|
|
1202
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
1203
|
+
method: "PUT",
|
|
1204
|
+
body: blob,
|
|
1205
|
+
headers: {
|
|
1206
|
+
"Content-Type": mimeType
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
if (!uploadResponse.ok) {
|
|
1210
|
+
throw new Error("Voice upload failed");
|
|
1211
|
+
}
|
|
1212
|
+
onProgress?.(75);
|
|
1213
|
+
await apiClient.confirmUpload(fileId);
|
|
1214
|
+
onProgress?.(100);
|
|
1215
|
+
return fileId;
|
|
1216
|
+
}
|
|
1217
|
+
function createVoiceMessageFileName(extension = "m4a") {
|
|
1218
|
+
return `voice-${Date.now()}.${extension}`;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
exports.ALLOWED_ATTACHMENT_MIME_TYPES = ALLOWED_ATTACHMENT_MIME_TYPES;
|
|
1222
|
+
exports.CLIENT_TOKEN_HEADER = CLIENT_TOKEN_HEADER;
|
|
1223
|
+
exports.Jokkoo = Jokkoo;
|
|
1224
|
+
exports.JokkooApiClient = JokkooApiClient;
|
|
1225
|
+
exports.JokkooApiError = JokkooApiError;
|
|
1226
|
+
exports.JokkooConfigError = JokkooConfigError;
|
|
1227
|
+
exports.JokkooRealtimeClient = JokkooRealtimeClient;
|
|
1228
|
+
exports.MAX_ATTACHMENTS_PER_MESSAGE = MAX_ATTACHMENTS_PER_MESSAGE;
|
|
1229
|
+
exports.MAX_AUDIO_SIZE_BYTES = MAX_AUDIO_SIZE_BYTES;
|
|
1230
|
+
exports.MAX_DOCUMENT_SIZE_BYTES = MAX_DOCUMENT_SIZE_BYTES;
|
|
1231
|
+
exports.MAX_IMAGE_SIZE_BYTES = MAX_IMAGE_SIZE_BYTES;
|
|
1232
|
+
exports.MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_BYTES;
|
|
1233
|
+
exports.MESSAGE_CONTENT_MAX_LENGTH = MESSAGE_CONTENT_MAX_LENGTH;
|
|
1234
|
+
exports.MESSAGE_CONTENT_MIN_LENGTH = MESSAGE_CONTENT_MIN_LENGTH;
|
|
1235
|
+
exports.NEW_CONVERSATION_ID = NEW_CONVERSATION_ID;
|
|
1236
|
+
exports.REALTIME_EVENTS = REALTIME_EVENTS;
|
|
1237
|
+
exports.REALTIME_NAMESPACE = REALTIME_NAMESPACE;
|
|
1238
|
+
exports.TokenManager = TokenManager;
|
|
1239
|
+
exports.USER_TOKEN_HEADER = USER_TOKEN_HEADER;
|
|
1240
|
+
exports.VOICE_MESSAGE_MIME_TYPE = VOICE_MESSAGE_MIME_TYPE;
|
|
1241
|
+
exports.VOICE_MESSAGE_STORAGE_PROVIDER = VOICE_MESSAGE_STORAGE_PROVIDER;
|
|
1242
|
+
exports.applyLiveBarVariation = applyLiveBarVariation;
|
|
1243
|
+
exports.blendLiveWaveformLevels = blendLiveWaveformLevels;
|
|
1244
|
+
exports.canAddMoreAttachments = canAddMoreAttachments;
|
|
1245
|
+
exports.canPreviewInApp = canPreviewInApp;
|
|
1246
|
+
exports.canReplyToClosedConversation = canReplyToClosedConversation;
|
|
1247
|
+
exports.conversationNeedsRating = conversationNeedsRating;
|
|
1248
|
+
exports.createDebugLogger = createDebugLogger;
|
|
1249
|
+
exports.createIdleMeteringLevels = createIdleMeteringLevels;
|
|
1250
|
+
exports.createPendingAttachment = createPendingAttachment;
|
|
1251
|
+
exports.createPlaybackBarHeights = createPlaybackBarHeights;
|
|
1252
|
+
exports.createVoiceMessageFileName = createVoiceMessageFileName;
|
|
1253
|
+
exports.createWelcomeMessage = createWelcomeMessage;
|
|
1254
|
+
exports.en = en;
|
|
1255
|
+
exports.fetchMessagesPage = fetchMessagesPage;
|
|
1256
|
+
exports.formatAttachmentSize = formatAttachmentSize;
|
|
1257
|
+
exports.formatAudioDuration = formatAudioDuration;
|
|
1258
|
+
exports.formatMessageTimestamp = formatMessageTimestamp;
|
|
1259
|
+
exports.formatRelativeTimestamp = formatRelativeTimestamp;
|
|
1260
|
+
exports.fr = fr;
|
|
1261
|
+
exports.getAgentDisplayName = getAgentDisplayName;
|
|
1262
|
+
exports.getAgentInitials = getAgentInitials;
|
|
1263
|
+
exports.getConversationDisplayName = getConversationDisplayName;
|
|
1264
|
+
exports.getInitialsFromName = getInitialsFromName;
|
|
1265
|
+
exports.getOlderMessagesPageParam = getOlderMessagesPageParam;
|
|
1266
|
+
exports.getSatisfactionSurveyMessage = getSatisfactionSurveyMessage;
|
|
1267
|
+
exports.getTranslations = getTranslations;
|
|
1268
|
+
exports.isAudioMimeType = isAudioMimeType;
|
|
1269
|
+
exports.isDocumentMimeType = isDocumentMimeType;
|
|
1270
|
+
exports.isImageMimeType = isImageMimeType;
|
|
1271
|
+
exports.isPassiveActivityMessage = isPassiveActivityMessage;
|
|
1272
|
+
exports.isSatisfactionRequestMessage = isSatisfactionRequestMessage;
|
|
1273
|
+
exports.isTicketActivityMessage = isTicketActivityMessage;
|
|
1274
|
+
exports.isVideoMimeType = isVideoMimeType;
|
|
1275
|
+
exports.isVoiceMessageMetadata = isVoiceMessageMetadata;
|
|
1276
|
+
exports.isWithinReopenGracePeriod = isWithinReopenGracePeriod;
|
|
1277
|
+
exports.mapApiAttachmentToChatAttachment = mapApiAttachmentToChatAttachment;
|
|
1278
|
+
exports.mapMessageToChatMessage = mapMessageToChatMessage;
|
|
1279
|
+
exports.mapRealtimeConversationPayload = mapRealtimeConversationPayload;
|
|
1280
|
+
exports.normalizeMeteringLevel = normalizeMeteringLevel;
|
|
1281
|
+
exports.normalizeRealtimeMessage = normalizeRealtimeMessage;
|
|
1282
|
+
exports.redactToken = redactToken;
|
|
1283
|
+
exports.shiftMeteringLevel = shiftMeteringLevel;
|
|
1284
|
+
exports.shouldCountMessageAsUnread = shouldCountMessageAsUnread;
|
|
1285
|
+
exports.smoothMeteringSample = smoothMeteringSample;
|
|
1286
|
+
exports.uploadAttachmentFile = uploadAttachmentFile;
|
|
1287
|
+
exports.uploadVoiceBlob = uploadVoiceBlob;
|
|
1288
|
+
exports.uploadVoiceRecording = uploadVoiceRecording;
|
|
1289
|
+
exports.validateConfig = validateConfig;
|
|
1290
|
+
//# sourceMappingURL=index.js.map
|
|
1291
|
+
//# sourceMappingURL=index.js.map
|