@v-tilt/browser 1.4.3 → 1.4.4
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/dist/array.full.js +1 -1
- package/dist/array.full.js.map +1 -1
- package/dist/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/extensions/chat/chat-wrapper.d.ts +1 -1
- package/dist/extensions/chat/chat.d.ts +2 -2
- package/dist/external-scripts-loader.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/storage.d.ts +68 -51
- package/dist/user-manager.d.ts +15 -1
- package/lib/entrypoints/external-scripts-loader.js +1 -1
- package/lib/extensions/chat/chat-wrapper.d.ts +1 -1
- package/lib/extensions/chat/chat-wrapper.js +12 -6
- package/lib/extensions/chat/chat.d.ts +2 -2
- package/lib/extensions/chat/chat.js +89 -44
- package/lib/storage.d.ts +68 -51
- package/lib/storage.js +306 -219
- package/lib/user-manager.d.ts +15 -1
- package/lib/user-manager.js +38 -6
- package/lib/vtilt.js +6 -2
- package/package.json +1 -1
package/dist/storage.d.ts
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified Storage Manager
|
|
2
|
+
* Unified Storage Manager
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Handles all storage operations for sessions and users with consistent
|
|
5
|
+
* cookie handling across different persistence methods.
|
|
6
6
|
*
|
|
7
7
|
* Storage methods:
|
|
8
|
-
* - cookie
|
|
9
|
-
* - localStorage
|
|
10
|
-
* - sessionStorage
|
|
11
|
-
* - localStorage+cookie
|
|
12
|
-
*
|
|
8
|
+
* - `cookie`: Browser cookies with cross-subdomain support
|
|
9
|
+
* - `localStorage`: Persistent local storage
|
|
10
|
+
* - `sessionStorage`: Tab-specific storage (cleared on tab close)
|
|
11
|
+
* - `localStorage+cookie`: Hybrid mode (default) - full data in localStorage,
|
|
12
|
+
* critical identity properties also in cookies for SSR support
|
|
13
|
+
* - `memory`: In-memory only (no persistence)
|
|
14
|
+
*
|
|
15
|
+
* The `localStorage+cookie` mode is designed for traditional server-side
|
|
16
|
+
* rendered websites where each page navigation reloads JavaScript:
|
|
17
|
+
* - Critical properties (anonymous_id, device_id, etc.) are stored in cookies
|
|
18
|
+
* - Cookies ensure identity persists across full page reloads
|
|
19
|
+
* - localStorage provides fast access for SPA-style navigation
|
|
20
|
+
* - Falls back to cookie-only if localStorage is unavailable
|
|
13
21
|
*/
|
|
14
22
|
import { PersistenceMethod } from "./types";
|
|
23
|
+
/**
|
|
24
|
+
* Critical properties persisted to cookies in `localStorage+cookie` mode.
|
|
25
|
+
* These ensure identity survives full page reloads in traditional SSR websites.
|
|
26
|
+
*/
|
|
27
|
+
export declare const COOKIE_PERSISTED_PROPERTIES: readonly ["__vt_anonymous_id", "__vt_device_id", "__vt_distinct_id", "__vt_user_state"];
|
|
28
|
+
/** Session cookie TTL: 30 minutes */
|
|
15
29
|
export declare const SESSION_COOKIE_MAX_AGE = 1800;
|
|
30
|
+
/** User cookie TTL: 1 year */
|
|
16
31
|
export declare const USER_COOKIE_MAX_AGE = 31536000;
|
|
17
32
|
export interface StorageOptions {
|
|
18
33
|
method: PersistenceMethod;
|
|
34
|
+
/** Enable cross-subdomain cookies (auto-detected if not specified) */
|
|
19
35
|
cross_subdomain?: boolean;
|
|
36
|
+
/** Use secure cookies (auto-detected from protocol if not specified) */
|
|
20
37
|
secure?: boolean;
|
|
38
|
+
/** Cookie SameSite attribute */
|
|
21
39
|
sameSite?: "Strict" | "Lax" | "None";
|
|
22
40
|
}
|
|
23
41
|
export interface StorageItem<T = string> {
|
|
@@ -25,13 +43,15 @@ export interface StorageItem<T = string> {
|
|
|
25
43
|
expiry?: number;
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Returns false for platforms
|
|
46
|
+
* Check if cross-subdomain cookies should be enabled.
|
|
47
|
+
* Returns false for shared hosting platforms to prevent cookie conflicts.
|
|
30
48
|
*/
|
|
31
49
|
export declare function shouldUseCrossSubdomainCookie(): boolean;
|
|
32
50
|
/**
|
|
33
51
|
* Unified Storage Manager
|
|
52
|
+
*
|
|
34
53
|
* Provides consistent storage operations across all persistence methods
|
|
54
|
+
* with automatic fallbacks and SSR support.
|
|
35
55
|
*/
|
|
36
56
|
export declare class StorageManager {
|
|
37
57
|
private method;
|
|
@@ -39,62 +59,59 @@ export declare class StorageManager {
|
|
|
39
59
|
private secure;
|
|
40
60
|
private sameSite;
|
|
41
61
|
private memoryStorage;
|
|
62
|
+
private _localStorageSupported;
|
|
42
63
|
constructor(options: StorageOptions);
|
|
64
|
+
/** Check if localStorage is available (result is cached) */
|
|
65
|
+
private isLocalStorageSupported;
|
|
66
|
+
/** Check if a key should be persisted to cookies */
|
|
67
|
+
private isCriticalProperty;
|
|
43
68
|
/**
|
|
44
|
-
* Get a value from storage
|
|
69
|
+
* Get a value from storage.
|
|
70
|
+
*
|
|
71
|
+
* For `localStorage+cookie` mode:
|
|
72
|
+
* - Critical properties: read cookie first (for SSR), fall back to localStorage
|
|
73
|
+
* - Non-critical properties: read from localStorage only
|
|
45
74
|
*/
|
|
46
75
|
get(key: string): string | null;
|
|
47
76
|
/**
|
|
48
|
-
* Set a value in storage
|
|
49
|
-
* @param maxAge
|
|
77
|
+
* Set a value in storage.
|
|
78
|
+
* @param maxAge Cookie max age in seconds (only for cookie-based storage)
|
|
79
|
+
*
|
|
80
|
+
* For `localStorage+cookie` mode:
|
|
81
|
+
* - All values stored in localStorage (if available)
|
|
82
|
+
* - Critical properties ALWAYS stored in cookies for SSR support
|
|
50
83
|
*/
|
|
51
84
|
set(key: string, value: string, maxAge?: number): void;
|
|
52
|
-
/**
|
|
53
|
-
* Remove a value from storage
|
|
54
|
-
*/
|
|
85
|
+
/** Remove a value from storage */
|
|
55
86
|
remove(key: string): void;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
87
|
+
private getLocalStoragePlusCookie;
|
|
88
|
+
private setLocalStoragePlusCookie;
|
|
89
|
+
private removeLocalStoragePlusCookie;
|
|
90
|
+
private getLocalStorage;
|
|
91
|
+
private setLocalStorage;
|
|
92
|
+
private removeLocalStorage;
|
|
93
|
+
private readFromSessionStorage;
|
|
94
|
+
private writeToSessionStorage;
|
|
95
|
+
private removeFromSessionStorage;
|
|
96
|
+
private getCookie;
|
|
97
|
+
private setCookie;
|
|
98
|
+
private removeCookie;
|
|
99
|
+
/** Get JSON value with expiry check */
|
|
60
100
|
getWithExpiry<T>(key: string): T | null;
|
|
61
|
-
/**
|
|
62
|
-
* Set JSON value with optional expiry
|
|
63
|
-
* @param ttlMs - Time to live in milliseconds
|
|
64
|
-
*/
|
|
101
|
+
/** Set JSON value with optional TTL */
|
|
65
102
|
setWithExpiry<T>(key: string, value: T, ttlMs?: number): void;
|
|
66
|
-
/**
|
|
67
|
-
* Get JSON value (no expiry check)
|
|
68
|
-
*/
|
|
103
|
+
/** Get parsed JSON value */
|
|
69
104
|
getJSON<T>(key: string): T | null;
|
|
70
|
-
/**
|
|
71
|
-
* Set JSON value
|
|
72
|
-
*/
|
|
105
|
+
/** Set JSON value */
|
|
73
106
|
setJSON<T>(key: string, value: T, maxAge?: number): void;
|
|
74
|
-
|
|
75
|
-
private setCookie;
|
|
76
|
-
private removeCookie;
|
|
77
|
-
private usesLocalStorage;
|
|
78
|
-
private usesSessionStorage;
|
|
79
|
-
/**
|
|
80
|
-
* Check if sessionStorage is available
|
|
81
|
-
*/
|
|
107
|
+
/** Check if sessionStorage is available */
|
|
82
108
|
canUseSessionStorage(): boolean;
|
|
83
|
-
/**
|
|
84
|
-
* Get direct access to sessionStorage (for window_id which always uses sessionStorage)
|
|
85
|
-
*/
|
|
109
|
+
/** Get sessionStorage instance (for window_id which always uses sessionStorage) */
|
|
86
110
|
getSessionStorage(): Storage | null;
|
|
87
|
-
/**
|
|
88
|
-
* Update storage method at runtime
|
|
89
|
-
*/
|
|
111
|
+
/** Update storage method at runtime */
|
|
90
112
|
setMethod(method: PersistenceMethod): void;
|
|
91
|
-
/**
|
|
92
|
-
* Get current storage method
|
|
93
|
-
*/
|
|
113
|
+
/** Get current storage method */
|
|
94
114
|
getMethod(): PersistenceMethod;
|
|
95
115
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Create a shared storage instance
|
|
98
|
-
* Use this for creating storage managers with consistent settings
|
|
99
|
-
*/
|
|
116
|
+
/** Factory function to create a StorageManager with default settings */
|
|
100
117
|
export declare function createStorageManager(method: PersistenceMethod, cross_subdomain?: boolean): StorageManager;
|
package/dist/user-manager.d.ts
CHANGED
|
@@ -94,7 +94,21 @@ export declare class UserManager {
|
|
|
94
94
|
*/
|
|
95
95
|
update_referrer_info(): void;
|
|
96
96
|
/**
|
|
97
|
-
* Load user identity from storage
|
|
97
|
+
* Load user identity from storage.
|
|
98
|
+
*
|
|
99
|
+
* For traditional SSR websites where each page navigation reloads JavaScript,
|
|
100
|
+
* identity MUST be persisted immediately when generated to ensure the same
|
|
101
|
+
* anonymous_id is used across all page loads.
|
|
102
|
+
*
|
|
103
|
+
* Flow:
|
|
104
|
+
* 1. Load from storage (reads cookies first for critical properties in SSR mode)
|
|
105
|
+
* 2. Generate new IDs if not found
|
|
106
|
+
* 3. Immediately persist to storage (saved to both localStorage and cookies)
|
|
107
|
+
*
|
|
108
|
+
* With `localStorage+cookie` persistence (default):
|
|
109
|
+
* - Critical properties are stored in cookies for SSR compatibility
|
|
110
|
+
* - Full data is stored in localStorage for fast SPA-style access
|
|
111
|
+
* - Cookies ensure identity persists across full page reloads
|
|
98
112
|
*/
|
|
99
113
|
private loadUserIdentity;
|
|
100
114
|
/**
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* Code config always takes precedence over dashboard settings.
|
|
32
32
|
*/
|
|
33
33
|
import type { VTilt } from "../../vtilt";
|
|
34
|
-
import type
|
|
34
|
+
import { type ChatConfig, type ChatChannelSummary, type ChatWidgetView } from "../../utils/globals";
|
|
35
35
|
import type { MessageCallback, TypingCallback, ConnectionCallback, Unsubscribe } from "./types";
|
|
36
36
|
/** Status when lazy loading is in progress */
|
|
37
37
|
export declare const CHAT_LOADING: "loading";
|
|
@@ -352,13 +352,16 @@ class ChatWrapper {
|
|
|
352
352
|
get _isChatEnabled() {
|
|
353
353
|
var _a;
|
|
354
354
|
// Code config takes precedence
|
|
355
|
-
if (this._config.enabled === false)
|
|
355
|
+
if (this._config.enabled === false) {
|
|
356
356
|
return false;
|
|
357
|
-
|
|
357
|
+
}
|
|
358
|
+
if (this._config.enabled === true) {
|
|
358
359
|
return true;
|
|
360
|
+
}
|
|
359
361
|
// Fall back to server config
|
|
360
|
-
if (((_a = this._serverConfig) === null || _a === void 0 ? void 0 : _a.enabled) === true)
|
|
362
|
+
if (((_a = this._serverConfig) === null || _a === void 0 ? void 0 : _a.enabled) === true) {
|
|
361
363
|
return true;
|
|
364
|
+
}
|
|
362
365
|
// Default: disabled unless explicitly enabled
|
|
363
366
|
return false;
|
|
364
367
|
}
|
|
@@ -367,8 +370,9 @@ class ChatWrapper {
|
|
|
367
370
|
* This enables "snippet-only" installation where widget configures from dashboard
|
|
368
371
|
*/
|
|
369
372
|
async _fetchServerSettings() {
|
|
370
|
-
if (this._configFetched)
|
|
373
|
+
if (this._configFetched) {
|
|
371
374
|
return;
|
|
375
|
+
}
|
|
372
376
|
const config = this._instance.getConfig();
|
|
373
377
|
const token = config.token;
|
|
374
378
|
const apiHost = config.api_host;
|
|
@@ -399,11 +403,13 @@ class ChatWrapper {
|
|
|
399
403
|
* This creates a lightweight bubble that loads the full widget on click
|
|
400
404
|
*/
|
|
401
405
|
_showBubble() {
|
|
402
|
-
if (!(globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.document))
|
|
406
|
+
if (!(globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.document)) {
|
|
403
407
|
return;
|
|
408
|
+
}
|
|
404
409
|
// Don't create if already exists
|
|
405
|
-
if (document.getElementById("vtilt-chat-bubble"))
|
|
410
|
+
if (document.getElementById("vtilt-chat-bubble")) {
|
|
406
411
|
return;
|
|
412
|
+
}
|
|
407
413
|
const mergedConfig = this.getMergedConfig();
|
|
408
414
|
const position = mergedConfig.position || "bottom-right";
|
|
409
415
|
const color = mergedConfig.color || "#6366f1";
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
|
|
3
3
|
*/
|
|
4
4
|
import type { VTilt } from "../../vtilt";
|
|
5
|
-
import type
|
|
6
|
-
import type
|
|
5
|
+
import { type ChatConfig, type ChatChannel, type ChatChannelSummary, type ChatWidgetView, type LazyLoadedChatInterface } from "../../utils/globals";
|
|
6
|
+
import { type MessageCallback, type TypingCallback, type ConnectionCallback, type Unsubscribe } from "./types";
|
|
7
7
|
export declare class LazyLoadedChat implements LazyLoadedChatInterface {
|
|
8
8
|
private _instance;
|
|
9
9
|
private _config;
|
|
@@ -117,8 +117,9 @@ class LazyLoadedChat {
|
|
|
117
117
|
// ============================================================================
|
|
118
118
|
open() {
|
|
119
119
|
var _a;
|
|
120
|
-
if (this._state.isOpen)
|
|
120
|
+
if (this._state.isOpen) {
|
|
121
121
|
return;
|
|
122
|
+
}
|
|
122
123
|
this._state.isOpen = true;
|
|
123
124
|
this._updateUI();
|
|
124
125
|
// Add opening animation
|
|
@@ -136,8 +137,9 @@ class LazyLoadedChat {
|
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
close() {
|
|
139
|
-
if (!this._state.isOpen)
|
|
140
|
+
if (!this._state.isOpen) {
|
|
140
141
|
return;
|
|
142
|
+
}
|
|
141
143
|
const timeOpen = this._getTimeOpen();
|
|
142
144
|
// Add closing animation
|
|
143
145
|
if (this._widget) {
|
|
@@ -210,11 +212,13 @@ class LazyLoadedChat {
|
|
|
210
212
|
this._state.channel = response.channel;
|
|
211
213
|
this._state.messages = response.messages || [];
|
|
212
214
|
this._state.currentView = "conversation";
|
|
213
|
-
this._state.agentLastReadAt =
|
|
215
|
+
this._state.agentLastReadAt =
|
|
216
|
+
response.channel.agent_last_read_at || null;
|
|
214
217
|
this._initialUserReadAt = response.channel.user_last_read_at || null;
|
|
215
218
|
this._connectRealtime();
|
|
216
|
-
if (this._state.isOpen)
|
|
219
|
+
if (this._state.isOpen) {
|
|
217
220
|
this._autoMarkAsRead();
|
|
221
|
+
}
|
|
218
222
|
}
|
|
219
223
|
}
|
|
220
224
|
catch (error) {
|
|
@@ -242,7 +246,8 @@ class LazyLoadedChat {
|
|
|
242
246
|
this._state.channel = response.channel;
|
|
243
247
|
this._state.messages = response.messages || [];
|
|
244
248
|
this._state.currentView = "conversation";
|
|
245
|
-
this._state.agentLastReadAt =
|
|
249
|
+
this._state.agentLastReadAt =
|
|
250
|
+
response.channel.agent_last_read_at || null;
|
|
246
251
|
this._initialUserReadAt = response.channel.user_last_read_at || null;
|
|
247
252
|
const newChannelSummary = {
|
|
248
253
|
id: response.channel.id,
|
|
@@ -286,7 +291,8 @@ class LazyLoadedChat {
|
|
|
286
291
|
? this._state.messages[this._state.messages.length - 1].content.substring(0, 100)
|
|
287
292
|
: this._state.channels[channelIndex].last_message_preview,
|
|
288
293
|
last_message_sender: this._state.messages.length > 0
|
|
289
|
-
? this._state.messages[this._state.messages.length - 1]
|
|
294
|
+
? this._state.messages[this._state.messages.length - 1]
|
|
295
|
+
.sender_type
|
|
290
296
|
: this._state.channels[channelIndex].last_message_sender,
|
|
291
297
|
unread_count: 0, // We just viewed it
|
|
292
298
|
};
|
|
@@ -304,8 +310,9 @@ class LazyLoadedChat {
|
|
|
304
310
|
// ============================================================================
|
|
305
311
|
async sendMessage(content) {
|
|
306
312
|
var _a, _b, _c, _d;
|
|
307
|
-
if (!content.trim())
|
|
313
|
+
if (!content.trim()) {
|
|
308
314
|
return;
|
|
315
|
+
}
|
|
309
316
|
// Ensure we're in conversation view with a channel
|
|
310
317
|
if (!this._state.channel || this._state.currentView !== "conversation") {
|
|
311
318
|
console.error(`${LOGGER_PREFIX} Cannot send message: not in conversation view`);
|
|
@@ -367,14 +374,18 @@ class LazyLoadedChat {
|
|
|
367
374
|
this._autoMarkAsRead();
|
|
368
375
|
}
|
|
369
376
|
_autoMarkAsRead() {
|
|
370
|
-
if (!this._state.channel || this._isMarkingRead)
|
|
377
|
+
if (!this._state.channel || this._isMarkingRead) {
|
|
371
378
|
return;
|
|
379
|
+
}
|
|
372
380
|
const latestMessage = this._state.messages[this._state.messages.length - 1];
|
|
373
|
-
if (!latestMessage)
|
|
381
|
+
if (!latestMessage) {
|
|
374
382
|
return;
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
}
|
|
384
|
+
const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
385
|
+
!this._isMessageReadByUser(m.created_at));
|
|
386
|
+
if (!hasUnreadAgentMessages) {
|
|
377
387
|
return;
|
|
388
|
+
}
|
|
378
389
|
this._state.unreadCount = 0;
|
|
379
390
|
this._isMarkingRead = true;
|
|
380
391
|
this._updateUI();
|
|
@@ -397,8 +408,9 @@ class LazyLoadedChat {
|
|
|
397
408
|
});
|
|
398
409
|
}
|
|
399
410
|
_isMessageReadByUser(messageCreatedAt) {
|
|
400
|
-
if (!this._initialUserReadAt)
|
|
411
|
+
if (!this._initialUserReadAt) {
|
|
401
412
|
return false;
|
|
413
|
+
}
|
|
402
414
|
return new Date(messageCreatedAt) <= new Date(this._initialUserReadAt);
|
|
403
415
|
}
|
|
404
416
|
// ============================================================================
|
|
@@ -408,24 +420,27 @@ class LazyLoadedChat {
|
|
|
408
420
|
this._messageCallbacks.push(callback);
|
|
409
421
|
return () => {
|
|
410
422
|
const index = this._messageCallbacks.indexOf(callback);
|
|
411
|
-
if (index > -1)
|
|
423
|
+
if (index > -1) {
|
|
412
424
|
this._messageCallbacks.splice(index, 1);
|
|
425
|
+
}
|
|
413
426
|
};
|
|
414
427
|
}
|
|
415
428
|
onTyping(callback) {
|
|
416
429
|
this._typingCallbacks.push(callback);
|
|
417
430
|
return () => {
|
|
418
431
|
const index = this._typingCallbacks.indexOf(callback);
|
|
419
|
-
if (index > -1)
|
|
432
|
+
if (index > -1) {
|
|
420
433
|
this._typingCallbacks.splice(index, 1);
|
|
434
|
+
}
|
|
421
435
|
};
|
|
422
436
|
}
|
|
423
437
|
onConnectionChange(callback) {
|
|
424
438
|
this._connectionCallbacks.push(callback);
|
|
425
439
|
return () => {
|
|
426
440
|
const index = this._connectionCallbacks.indexOf(callback);
|
|
427
|
-
if (index > -1)
|
|
441
|
+
if (index > -1) {
|
|
428
442
|
this._connectionCallbacks.splice(index, 1);
|
|
443
|
+
}
|
|
429
444
|
};
|
|
430
445
|
}
|
|
431
446
|
// ============================================================================
|
|
@@ -434,8 +449,9 @@ class LazyLoadedChat {
|
|
|
434
449
|
destroy() {
|
|
435
450
|
var _a;
|
|
436
451
|
this._disconnectRealtime();
|
|
437
|
-
if (this._typingDebounce)
|
|
452
|
+
if (this._typingDebounce) {
|
|
438
453
|
clearTimeout(this._typingDebounce);
|
|
454
|
+
}
|
|
439
455
|
if ((_a = this._container) === null || _a === void 0 ? void 0 : _a.parentNode) {
|
|
440
456
|
this._container.parentNode.removeChild(this._container);
|
|
441
457
|
}
|
|
@@ -549,11 +565,15 @@ class LazyLoadedChat {
|
|
|
549
565
|
return parts[0] || "";
|
|
550
566
|
}
|
|
551
567
|
_handleNewMessage(message) {
|
|
552
|
-
if (this._state.messages.some((m) => m.id === message.id))
|
|
568
|
+
if (this._state.messages.some((m) => m.id === message.id)) {
|
|
553
569
|
return;
|
|
570
|
+
}
|
|
554
571
|
// Skip own messages but replace temp message if present
|
|
555
|
-
if (message.sender_type === "user" &&
|
|
556
|
-
|
|
572
|
+
if (message.sender_type === "user" &&
|
|
573
|
+
message.sender_id === this._distinctId) {
|
|
574
|
+
const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
|
|
575
|
+
m.content === message.content &&
|
|
576
|
+
m.sender_type === "user");
|
|
557
577
|
if (tempIndex !== -1) {
|
|
558
578
|
this._state.messages[tempIndex] = message;
|
|
559
579
|
this._updateUI();
|
|
@@ -581,17 +601,20 @@ class LazyLoadedChat {
|
|
|
581
601
|
this._updateUI();
|
|
582
602
|
}
|
|
583
603
|
_handleTypingEvent(event) {
|
|
584
|
-
if (event.sender_type === "user")
|
|
604
|
+
if (event.sender_type === "user") {
|
|
585
605
|
return;
|
|
586
|
-
|
|
606
|
+
}
|
|
607
|
+
const senderName = event.sender_name ||
|
|
608
|
+
(event.sender_type === "ai" ? "AI Assistant" : "Agent");
|
|
587
609
|
this._state.isTyping = event.is_typing;
|
|
588
610
|
this._state.typingSender = event.is_typing ? senderName : null;
|
|
589
611
|
this._typingCallbacks.forEach((cb) => cb(event.is_typing, senderName));
|
|
590
612
|
this._updateUI();
|
|
591
613
|
}
|
|
592
614
|
_handleReadCursorEvent(event) {
|
|
593
|
-
if (event.reader_type !== "agent")
|
|
615
|
+
if (event.reader_type !== "agent") {
|
|
594
616
|
return;
|
|
617
|
+
}
|
|
595
618
|
this._state.agentLastReadAt = event.read_at;
|
|
596
619
|
this._updateUI();
|
|
597
620
|
}
|
|
@@ -602,8 +625,9 @@ class LazyLoadedChat {
|
|
|
602
625
|
// Private - UI
|
|
603
626
|
// ============================================================================
|
|
604
627
|
_createUI() {
|
|
605
|
-
if (!globals_1.document)
|
|
628
|
+
if (!globals_1.document) {
|
|
606
629
|
return;
|
|
630
|
+
}
|
|
607
631
|
this._container = globals_1.document.createElement("div");
|
|
608
632
|
this._container.id = "vtilt-chat-container";
|
|
609
633
|
this._container.setAttribute("style", this._getContainerStyles());
|
|
@@ -625,22 +649,25 @@ class LazyLoadedChat {
|
|
|
625
649
|
(_c = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close")) === null || _c === void 0 ? void 0 : _c.addEventListener("click", () => this.close());
|
|
626
650
|
}
|
|
627
651
|
_handleUserTyping() {
|
|
628
|
-
if (!this._typingChannel)
|
|
652
|
+
if (!this._typingChannel) {
|
|
629
653
|
return;
|
|
654
|
+
}
|
|
630
655
|
if (!this._isUserTyping) {
|
|
631
656
|
this._isUserTyping = true;
|
|
632
657
|
this._sendTypingIndicator(true);
|
|
633
658
|
}
|
|
634
|
-
if (this._typingDebounce)
|
|
659
|
+
if (this._typingDebounce) {
|
|
635
660
|
clearTimeout(this._typingDebounce);
|
|
661
|
+
}
|
|
636
662
|
this._typingDebounce = setTimeout(() => {
|
|
637
663
|
this._isUserTyping = false;
|
|
638
664
|
this._sendTypingIndicator(false);
|
|
639
665
|
}, 2000);
|
|
640
666
|
}
|
|
641
667
|
_sendTypingIndicator(isTyping) {
|
|
642
|
-
if (!this._typingChannel)
|
|
668
|
+
if (!this._typingChannel) {
|
|
643
669
|
return;
|
|
670
|
+
}
|
|
644
671
|
try {
|
|
645
672
|
this._typingChannel.publish("typing", {
|
|
646
673
|
sender_type: "user",
|
|
@@ -656,8 +683,9 @@ class LazyLoadedChat {
|
|
|
656
683
|
_handleSend() {
|
|
657
684
|
var _a;
|
|
658
685
|
const input = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-input");
|
|
659
|
-
if (!input)
|
|
686
|
+
if (!input) {
|
|
660
687
|
return;
|
|
688
|
+
}
|
|
661
689
|
const content = input.value.trim();
|
|
662
690
|
if (content) {
|
|
663
691
|
if (this._isUserTyping) {
|
|
@@ -673,8 +701,9 @@ class LazyLoadedChat {
|
|
|
673
701
|
}
|
|
674
702
|
}
|
|
675
703
|
_updateUI() {
|
|
676
|
-
if (!this._container || !this._widget || !this._bubble)
|
|
704
|
+
if (!this._container || !this._widget || !this._bubble) {
|
|
677
705
|
return;
|
|
706
|
+
}
|
|
678
707
|
// Update visibility
|
|
679
708
|
this._container.style.display = this._state.isVisible ? "block" : "none";
|
|
680
709
|
// Update widget open state
|
|
@@ -724,8 +753,9 @@ class LazyLoadedChat {
|
|
|
724
753
|
_updateHeader() {
|
|
725
754
|
var _a, _b;
|
|
726
755
|
const header = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-header");
|
|
727
|
-
if (!header)
|
|
756
|
+
if (!header) {
|
|
728
757
|
return;
|
|
758
|
+
}
|
|
729
759
|
const greeting = this._config.greeting || "Messages";
|
|
730
760
|
const primary = this._theme.primaryColor;
|
|
731
761
|
if (this._state.currentView === "list") {
|
|
@@ -981,15 +1011,17 @@ class LazyLoadedChat {
|
|
|
981
1011
|
flex: 1;
|
|
982
1012
|
min-width: 0;
|
|
983
1013
|
line-height: 1.4;
|
|
984
|
-
">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ?
|
|
985
|
-
${hasUnread
|
|
1014
|
+
">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? " · Closed" : ""}</div>
|
|
1015
|
+
${hasUnread
|
|
1016
|
+
? `<div style="
|
|
986
1017
|
min-width: 10px;
|
|
987
1018
|
width: 10px;
|
|
988
1019
|
height: 10px;
|
|
989
1020
|
background: ${primary};
|
|
990
1021
|
border-radius: 50%;
|
|
991
1022
|
flex-shrink: 0;
|
|
992
|
-
"></div>`
|
|
1023
|
+
"></div>`
|
|
1024
|
+
: ""}
|
|
993
1025
|
</div>
|
|
994
1026
|
</div>
|
|
995
1027
|
</div>
|
|
@@ -1097,8 +1129,9 @@ class LazyLoadedChat {
|
|
|
1097
1129
|
(_d = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelectorAll(".vtilt-channel-item")) === null || _d === void 0 ? void 0 : _d.forEach((item) => {
|
|
1098
1130
|
item.addEventListener("click", () => {
|
|
1099
1131
|
const channelId = item.getAttribute("data-channel-id");
|
|
1100
|
-
if (channelId)
|
|
1132
|
+
if (channelId) {
|
|
1101
1133
|
this.selectChannel(channelId);
|
|
1134
|
+
}
|
|
1102
1135
|
});
|
|
1103
1136
|
});
|
|
1104
1137
|
}
|
|
@@ -1121,29 +1154,37 @@ class LazyLoadedChat {
|
|
|
1121
1154
|
const diffMins = Math.floor(diffMs / 60000);
|
|
1122
1155
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
1123
1156
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
1124
|
-
if (diffMins < 1)
|
|
1157
|
+
if (diffMins < 1) {
|
|
1125
1158
|
return "Just now";
|
|
1126
|
-
|
|
1159
|
+
}
|
|
1160
|
+
if (diffMins < 60) {
|
|
1127
1161
|
return `${diffMins}m ago`;
|
|
1128
|
-
|
|
1162
|
+
}
|
|
1163
|
+
if (diffHours < 24) {
|
|
1129
1164
|
return `${diffHours}h ago`;
|
|
1130
|
-
|
|
1165
|
+
}
|
|
1166
|
+
if (diffDays < 7) {
|
|
1131
1167
|
return `${diffDays}d ago`;
|
|
1168
|
+
}
|
|
1132
1169
|
return date.toLocaleDateString();
|
|
1133
1170
|
}
|
|
1134
1171
|
_renderMessages() {
|
|
1135
1172
|
var _a;
|
|
1136
1173
|
const container = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
|
|
1137
|
-
if (!container)
|
|
1174
|
+
if (!container) {
|
|
1138
1175
|
return;
|
|
1176
|
+
}
|
|
1139
1177
|
const primary = this._theme.primaryColor;
|
|
1140
|
-
const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
1141
|
-
|
|
1178
|
+
const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
1179
|
+
!this._isMessageReadByUser(m.created_at));
|
|
1180
|
+
const html = this._state.messages
|
|
1181
|
+
.map((msg, i) => {
|
|
1142
1182
|
const divider = i === firstUnreadIndex && firstUnreadIndex > 0
|
|
1143
1183
|
? `<div style="display:flex;align-items:center;gap:12px;margin:12px 0"><div style="flex:1;height:1px;background:#DDD"></div><span style="font-size:12px;font-weight:600;color:${primary}">New</span><div style="flex:1;height:1px;background:#DDD"></div></div>`
|
|
1144
1184
|
: "";
|
|
1145
1185
|
return divider + this._getMessageHTML(msg);
|
|
1146
|
-
})
|
|
1186
|
+
})
|
|
1187
|
+
.join("");
|
|
1147
1188
|
container.innerHTML = html;
|
|
1148
1189
|
container.scrollTop = container.scrollHeight;
|
|
1149
1190
|
}
|
|
@@ -1375,7 +1416,9 @@ class LazyLoadedChat {
|
|
|
1375
1416
|
</div>
|
|
1376
1417
|
`;
|
|
1377
1418
|
}
|
|
1378
|
-
const senderLabel = isAi
|
|
1419
|
+
const senderLabel = isAi
|
|
1420
|
+
? "AI Assistant"
|
|
1421
|
+
: message.sender_name || "Support";
|
|
1379
1422
|
return `
|
|
1380
1423
|
<div class="vtilt-msg" style="
|
|
1381
1424
|
display: flex;
|
|
@@ -1416,8 +1459,9 @@ class LazyLoadedChat {
|
|
|
1416
1459
|
`;
|
|
1417
1460
|
}
|
|
1418
1461
|
_isMessageReadByAgent(messageCreatedAt) {
|
|
1419
|
-
if (!this._state.agentLastReadAt)
|
|
1462
|
+
if (!this._state.agentLastReadAt) {
|
|
1420
1463
|
return false;
|
|
1464
|
+
}
|
|
1421
1465
|
return new Date(messageCreatedAt) <= new Date(this._state.agentLastReadAt);
|
|
1422
1466
|
}
|
|
1423
1467
|
// ============================================================================
|
|
@@ -1456,8 +1500,9 @@ class LazyLoadedChat {
|
|
|
1456
1500
|
return 0;
|
|
1457
1501
|
}
|
|
1458
1502
|
_escapeHTML(text) {
|
|
1459
|
-
if (!globals_1.document)
|
|
1503
|
+
if (!globals_1.document) {
|
|
1460
1504
|
return text;
|
|
1505
|
+
}
|
|
1461
1506
|
const div = globals_1.document.createElement("div");
|
|
1462
1507
|
div.textContent = text;
|
|
1463
1508
|
return div.innerHTML;
|