@product7/feedback-sdk 1.2.6 → 1.2.7
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 +35 -35
- package/dist/README.md +35 -35
- package/dist/feedback-sdk.js +719 -660
- package/dist/feedback-sdk.js.map +1 -1
- package/dist/feedback-sdk.min.js +1 -1
- package/dist/feedback-sdk.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/APIService.js +66 -41
- package/src/core/FeedbackSDK.js +5 -3
- package/src/core/WebSocketService.js +3 -1
- package/src/widgets/BaseWidget.js +6 -4
- package/src/widgets/MessengerWidget.js +22 -7
- package/src/widgets/messenger/views/ChatView.js +12 -4
- package/src/widgets/messenger/views/HomeView.js +7 -2
- package/types/index.d.ts +49 -0
package/dist/feedback-sdk.js
CHANGED
|
@@ -158,7 +158,9 @@
|
|
|
158
158
|
id: 'conv_2',
|
|
159
159
|
subject: 'Feature request',
|
|
160
160
|
status: 'open',
|
|
161
|
-
last_message_at: new Date(
|
|
161
|
+
last_message_at: new Date(
|
|
162
|
+
Date.now() - 6 * 24 * 60 * 60 * 1000
|
|
163
|
+
).toISOString(),
|
|
162
164
|
created_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
163
165
|
unread: 0,
|
|
164
166
|
assigned_user: {
|
|
@@ -200,11 +202,14 @@
|
|
|
200
202
|
id: 'msg_4',
|
|
201
203
|
content: 'I would love to see a dark mode feature!',
|
|
202
204
|
sender_type: 'customer',
|
|
203
|
-
created_at: new Date(
|
|
205
|
+
created_at: new Date(
|
|
206
|
+
Date.now() - 6 * 24 * 60 * 60 * 1000 - 30 * 60 * 1000
|
|
207
|
+
).toISOString(),
|
|
204
208
|
},
|
|
205
209
|
{
|
|
206
210
|
id: 'msg_5',
|
|
207
|
-
content:
|
|
211
|
+
content:
|
|
212
|
+
"Great suggestion! That feature will be available next week. I'll let you know when it's ready.",
|
|
208
213
|
sender_type: 'agent',
|
|
209
214
|
sender_name: 'Tom',
|
|
210
215
|
sender_avatar: null,
|
|
@@ -234,7 +239,8 @@
|
|
|
234
239
|
{
|
|
235
240
|
id: 'collection_3',
|
|
236
241
|
title: 'AI Agent',
|
|
237
|
-
description:
|
|
242
|
+
description:
|
|
243
|
+
'Resolving customer questions instantly and accurately—from live chat to email.',
|
|
238
244
|
articleCount: 82,
|
|
239
245
|
icon: 'ph-robot',
|
|
240
246
|
url: '#',
|
|
@@ -242,7 +248,8 @@
|
|
|
242
248
|
{
|
|
243
249
|
id: 'collection_4',
|
|
244
250
|
title: 'Channels',
|
|
245
|
-
description:
|
|
251
|
+
description:
|
|
252
|
+
'Enabling the channels you use to communicate with customers, all from the Inbox.',
|
|
246
253
|
articleCount: 45,
|
|
247
254
|
icon: 'ph-chat-circle',
|
|
248
255
|
url: '#',
|
|
@@ -789,10 +796,13 @@
|
|
|
789
796
|
};
|
|
790
797
|
}
|
|
791
798
|
|
|
792
|
-
return this._makeRequest(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
799
|
+
return this._makeRequest(
|
|
800
|
+
`/widget/messenger/conversations/${conversationId}`,
|
|
801
|
+
{
|
|
802
|
+
method: 'GET',
|
|
803
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
804
|
+
}
|
|
805
|
+
);
|
|
796
806
|
}
|
|
797
807
|
|
|
798
808
|
/**
|
|
@@ -899,14 +909,17 @@
|
|
|
899
909
|
return { status: true, data: newMessage };
|
|
900
910
|
}
|
|
901
911
|
|
|
902
|
-
return this._makeRequest(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
912
|
+
return this._makeRequest(
|
|
913
|
+
`/widget/messenger/conversations/${conversationId}/messages`,
|
|
914
|
+
{
|
|
915
|
+
method: 'POST',
|
|
916
|
+
headers: {
|
|
917
|
+
'Content-Type': 'application/json',
|
|
918
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
919
|
+
},
|
|
920
|
+
body: JSON.stringify({ content: data.content }),
|
|
921
|
+
}
|
|
922
|
+
);
|
|
910
923
|
}
|
|
911
924
|
|
|
912
925
|
/**
|
|
@@ -924,14 +937,17 @@
|
|
|
924
937
|
return { status: true };
|
|
925
938
|
}
|
|
926
939
|
|
|
927
|
-
return this._makeRequest(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
940
|
+
return this._makeRequest(
|
|
941
|
+
`/widget/messenger/conversations/${conversationId}/typing`,
|
|
942
|
+
{
|
|
943
|
+
method: 'POST',
|
|
944
|
+
headers: {
|
|
945
|
+
'Content-Type': 'application/json',
|
|
946
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
947
|
+
},
|
|
948
|
+
body: JSON.stringify({ is_typing: isTyping }),
|
|
949
|
+
}
|
|
950
|
+
);
|
|
935
951
|
}
|
|
936
952
|
|
|
937
953
|
/**
|
|
@@ -948,10 +964,13 @@
|
|
|
948
964
|
return { status: true };
|
|
949
965
|
}
|
|
950
966
|
|
|
951
|
-
return this._makeRequest(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
967
|
+
return this._makeRequest(
|
|
968
|
+
`/widget/messenger/conversations/${conversationId}/read`,
|
|
969
|
+
{
|
|
970
|
+
method: 'POST',
|
|
971
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
972
|
+
}
|
|
973
|
+
);
|
|
955
974
|
}
|
|
956
975
|
|
|
957
976
|
/**
|
|
@@ -964,7 +983,10 @@
|
|
|
964
983
|
}
|
|
965
984
|
|
|
966
985
|
if (this.mock) {
|
|
967
|
-
const count = MOCK_CONVERSATIONS.reduce(
|
|
986
|
+
const count = MOCK_CONVERSATIONS.reduce(
|
|
987
|
+
(sum, c) => sum + (c.unread || 0),
|
|
988
|
+
0
|
|
989
|
+
);
|
|
968
990
|
return {
|
|
969
991
|
status: true,
|
|
970
992
|
data: { unread_count: count, unread_conversations: count > 0 ? 1 : 0 },
|
|
@@ -994,17 +1016,20 @@
|
|
|
994
1016
|
return { status: true, message: 'Thank you for your feedback!' };
|
|
995
1017
|
}
|
|
996
1018
|
|
|
997
|
-
return this._makeRequest(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1019
|
+
return this._makeRequest(
|
|
1020
|
+
`/widget/messenger/conversations/${conversationId}/rate`,
|
|
1021
|
+
{
|
|
1022
|
+
method: 'POST',
|
|
1023
|
+
headers: {
|
|
1024
|
+
'Content-Type': 'application/json',
|
|
1025
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
1026
|
+
},
|
|
1027
|
+
body: JSON.stringify({
|
|
1028
|
+
rating: data.rating,
|
|
1029
|
+
comment: data.comment || '',
|
|
1030
|
+
}),
|
|
1031
|
+
}
|
|
1032
|
+
);
|
|
1008
1033
|
}
|
|
1009
1034
|
|
|
1010
1035
|
/**
|
|
@@ -1602,7 +1627,7 @@
|
|
|
1602
1627
|
if (this.options.suppressAfterSubmission && this._hasRecentlySubmitted()) {
|
|
1603
1628
|
this.sdk.eventBus.emit('widget:suppressed', {
|
|
1604
1629
|
widget: this,
|
|
1605
|
-
reason: 'recently_submitted'
|
|
1630
|
+
reason: 'recently_submitted',
|
|
1606
1631
|
});
|
|
1607
1632
|
return this;
|
|
1608
1633
|
}
|
|
@@ -1826,8 +1851,10 @@
|
|
|
1826
1851
|
// Check backend tracking first (from init response)
|
|
1827
1852
|
if (this.sdk.config.last_feedback_at) {
|
|
1828
1853
|
try {
|
|
1829
|
-
const backendTimestamp = new Date(
|
|
1830
|
-
|
|
1854
|
+
const backendTimestamp = new Date(
|
|
1855
|
+
this.sdk.config.last_feedback_at
|
|
1856
|
+
).getTime();
|
|
1857
|
+
if (now - backendTimestamp < cooldownMs) {
|
|
1831
1858
|
return true;
|
|
1832
1859
|
}
|
|
1833
1860
|
} catch (e) {
|
|
@@ -1846,7 +1873,7 @@
|
|
|
1846
1873
|
const data = JSON.parse(stored);
|
|
1847
1874
|
const submittedAt = data.submittedAt;
|
|
1848
1875
|
|
|
1849
|
-
return
|
|
1876
|
+
return now - submittedAt < cooldownMs;
|
|
1850
1877
|
} catch (e) {
|
|
1851
1878
|
// localStorage may not be available or data is corrupted
|
|
1852
1879
|
return false;
|
|
@@ -2971,135 +2998,411 @@
|
|
|
2971
2998
|
}
|
|
2972
2999
|
|
|
2973
3000
|
/**
|
|
2974
|
-
*
|
|
3001
|
+
* WebSocketService - Real-time communication for messenger widget
|
|
2975
3002
|
*/
|
|
2976
|
-
class MessengerState {
|
|
2977
|
-
constructor(options = {}) {
|
|
2978
|
-
this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
|
|
2979
|
-
this.isOpen = false;
|
|
2980
|
-
this.unreadCount = 0;
|
|
2981
|
-
this.activeConversationId = null;
|
|
2982
|
-
|
|
2983
|
-
// Conversations
|
|
2984
|
-
this.conversations = [];
|
|
2985
|
-
this.messages = {}; // { conversationId: [messages] }
|
|
2986
3003
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
this.
|
|
3004
|
+
class WebSocketService {
|
|
3005
|
+
constructor(config = {}) {
|
|
3006
|
+
this.baseURL = config.baseURL || '';
|
|
3007
|
+
this.workspace = config.workspace || '';
|
|
3008
|
+
this.sessionToken = config.sessionToken || null;
|
|
3009
|
+
this.mock = config.mock || false;
|
|
2990
3010
|
|
|
2991
|
-
|
|
2992
|
-
this.
|
|
2993
|
-
this.
|
|
3011
|
+
this.ws = null;
|
|
3012
|
+
this.reconnectAttempts = 0;
|
|
3013
|
+
this.maxReconnectAttempts = 5;
|
|
3014
|
+
this.reconnectDelay = 1000;
|
|
3015
|
+
this.pingInterval = null;
|
|
3016
|
+
this.isConnected = false;
|
|
2994
3017
|
|
|
2995
|
-
//
|
|
2996
|
-
this.
|
|
2997
|
-
this.teamAvatars = options.teamAvatars || [];
|
|
2998
|
-
this.welcomeMessage = options.welcomeMessage || 'How can we help?';
|
|
3018
|
+
// Event listeners
|
|
3019
|
+
this._listeners = new Map();
|
|
2999
3020
|
|
|
3000
|
-
//
|
|
3001
|
-
this.
|
|
3021
|
+
// Bind methods
|
|
3022
|
+
this._onOpen = this._onOpen.bind(this);
|
|
3023
|
+
this._onMessage = this._onMessage.bind(this);
|
|
3024
|
+
this._onClose = this._onClose.bind(this);
|
|
3025
|
+
this._onError = this._onError.bind(this);
|
|
3026
|
+
}
|
|
3002
3027
|
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3028
|
+
/**
|
|
3029
|
+
* Connect to WebSocket server
|
|
3030
|
+
*/
|
|
3031
|
+
connect(sessionToken = null) {
|
|
3032
|
+
if (sessionToken) {
|
|
3033
|
+
this.sessionToken = sessionToken;
|
|
3034
|
+
}
|
|
3006
3035
|
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3036
|
+
if (!this.sessionToken) {
|
|
3037
|
+
console.warn('[WebSocket] No session token provided');
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3011
3040
|
|
|
3012
|
-
//
|
|
3013
|
-
this.
|
|
3041
|
+
// Mock mode - simulate connection
|
|
3042
|
+
if (this.mock) {
|
|
3043
|
+
this.isConnected = true;
|
|
3044
|
+
this._emit('connected', {});
|
|
3045
|
+
this._startMockResponses();
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3014
3048
|
|
|
3015
|
-
//
|
|
3016
|
-
|
|
3017
|
-
this.
|
|
3049
|
+
// Build WebSocket URL
|
|
3050
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
3051
|
+
let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
|
|
3052
|
+
wsURL = wsURL.replace('/api/v1', '');
|
|
3053
|
+
wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
|
|
3018
3054
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3055
|
+
try {
|
|
3056
|
+
this.ws = new WebSocket(wsURL);
|
|
3057
|
+
this.ws.onopen = this._onOpen;
|
|
3058
|
+
this.ws.onmessage = this._onMessage;
|
|
3059
|
+
this.ws.onclose = this._onClose;
|
|
3060
|
+
this.ws.onerror = this._onError;
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
console.error('[WebSocket] Connection error:', error);
|
|
3063
|
+
this._scheduleReconnect();
|
|
3064
|
+
}
|
|
3021
3065
|
}
|
|
3022
3066
|
|
|
3023
3067
|
/**
|
|
3024
|
-
*
|
|
3068
|
+
* Disconnect from WebSocket server
|
|
3025
3069
|
*/
|
|
3026
|
-
|
|
3027
|
-
this.
|
|
3028
|
-
|
|
3029
|
-
}
|
|
3070
|
+
disconnect() {
|
|
3071
|
+
this.isConnected = false;
|
|
3072
|
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
3030
3073
|
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
this._listeners.forEach((cb) => cb(changeType, data, this));
|
|
3036
|
-
}
|
|
3074
|
+
if (this.pingInterval) {
|
|
3075
|
+
clearInterval(this.pingInterval);
|
|
3076
|
+
this.pingInterval = null;
|
|
3077
|
+
}
|
|
3037
3078
|
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
const previousView = this.currentView;
|
|
3043
|
-
this.currentView = view;
|
|
3044
|
-
this._notify('viewChange', { previousView, currentView: view });
|
|
3045
|
-
}
|
|
3079
|
+
if (this.ws) {
|
|
3080
|
+
this.ws.close();
|
|
3081
|
+
this.ws = null;
|
|
3082
|
+
}
|
|
3046
3083
|
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
this.isOpen = isOpen;
|
|
3052
|
-
this._notify('openChange', { isOpen });
|
|
3084
|
+
if (this._mockInterval) {
|
|
3085
|
+
clearInterval(this._mockInterval);
|
|
3086
|
+
this._mockInterval = null;
|
|
3087
|
+
}
|
|
3053
3088
|
}
|
|
3054
3089
|
|
|
3055
3090
|
/**
|
|
3056
|
-
*
|
|
3091
|
+
* Subscribe to events
|
|
3092
|
+
* @param {string} event - Event name
|
|
3093
|
+
* @param {Function} callback - Event handler
|
|
3094
|
+
* @returns {Function} Unsubscribe function
|
|
3057
3095
|
*/
|
|
3058
|
-
|
|
3059
|
-
this.
|
|
3060
|
-
|
|
3096
|
+
on(event, callback) {
|
|
3097
|
+
if (!this._listeners.has(event)) {
|
|
3098
|
+
this._listeners.set(event, new Set());
|
|
3099
|
+
}
|
|
3100
|
+
this._listeners.get(event).add(callback);
|
|
3101
|
+
return () => this._listeners.get(event).delete(callback);
|
|
3061
3102
|
}
|
|
3062
3103
|
|
|
3063
3104
|
/**
|
|
3064
|
-
*
|
|
3105
|
+
* Remove event listener
|
|
3065
3106
|
*/
|
|
3066
|
-
|
|
3067
|
-
this.
|
|
3068
|
-
|
|
3069
|
-
|
|
3107
|
+
off(event, callback) {
|
|
3108
|
+
if (this._listeners.has(event)) {
|
|
3109
|
+
this._listeners.get(event).delete(callback);
|
|
3110
|
+
}
|
|
3070
3111
|
}
|
|
3071
3112
|
|
|
3072
3113
|
/**
|
|
3073
|
-
*
|
|
3114
|
+
* Send message through WebSocket
|
|
3074
3115
|
*/
|
|
3075
|
-
|
|
3076
|
-
this.
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3116
|
+
send(type, payload = {}) {
|
|
3117
|
+
if (!this.isConnected) {
|
|
3118
|
+
console.warn('[WebSocket] Not connected, cannot send message');
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3080
3121
|
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
this._notify('messagesUpdate', { conversationId, messages });
|
|
3087
|
-
}
|
|
3122
|
+
if (this.mock) {
|
|
3123
|
+
// Mock mode - just log
|
|
3124
|
+
console.log('[WebSocket Mock] Sending:', type, payload);
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3088
3127
|
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
*/
|
|
3092
|
-
addMessage(conversationId, message) {
|
|
3093
|
-
if (!this.messages[conversationId]) {
|
|
3094
|
-
this.messages[conversationId] = [];
|
|
3128
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
3129
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
3095
3130
|
}
|
|
3096
|
-
|
|
3131
|
+
}
|
|
3097
3132
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3133
|
+
// Private methods
|
|
3134
|
+
|
|
3135
|
+
_onOpen() {
|
|
3136
|
+
console.log('[WebSocket] Connected');
|
|
3137
|
+
this.isConnected = true;
|
|
3138
|
+
this.reconnectAttempts = 0;
|
|
3139
|
+
this._emit('connected', {});
|
|
3140
|
+
|
|
3141
|
+
// Start ping interval to keep connection alive
|
|
3142
|
+
this.pingInterval = setInterval(() => {
|
|
3143
|
+
this.send('ping', {});
|
|
3144
|
+
}, 30000);
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
_onMessage(event) {
|
|
3148
|
+
try {
|
|
3149
|
+
const data = JSON.parse(event.data);
|
|
3150
|
+
const { type, payload } = data;
|
|
3151
|
+
|
|
3152
|
+
// Handle different event types
|
|
3153
|
+
switch (type) {
|
|
3154
|
+
case 'message:new':
|
|
3155
|
+
this._emit('message', payload);
|
|
3156
|
+
break;
|
|
3157
|
+
case 'typing:started':
|
|
3158
|
+
this._emit('typing_started', payload);
|
|
3159
|
+
break;
|
|
3160
|
+
case 'typing:stopped':
|
|
3161
|
+
this._emit('typing_stopped', payload);
|
|
3162
|
+
break;
|
|
3163
|
+
case 'conversation:updated':
|
|
3164
|
+
this._emit('conversation_updated', payload);
|
|
3165
|
+
break;
|
|
3166
|
+
case 'conversation:closed':
|
|
3167
|
+
this._emit('conversation_closed', payload);
|
|
3168
|
+
break;
|
|
3169
|
+
case 'availability:changed':
|
|
3170
|
+
this._emit('availability_changed', payload);
|
|
3171
|
+
break;
|
|
3172
|
+
case 'pong':
|
|
3173
|
+
// Ping response, ignore
|
|
3174
|
+
break;
|
|
3175
|
+
default:
|
|
3176
|
+
console.log('[WebSocket] Unknown event:', type, payload);
|
|
3177
|
+
}
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
console.error('[WebSocket] Failed to parse message:', error);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
_onClose(event) {
|
|
3184
|
+
console.log('[WebSocket] Disconnected:', event.code, event.reason);
|
|
3185
|
+
this.isConnected = false;
|
|
3186
|
+
|
|
3187
|
+
if (this.pingInterval) {
|
|
3188
|
+
clearInterval(this.pingInterval);
|
|
3189
|
+
this.pingInterval = null;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
this._emit('disconnected', { code: event.code, reason: event.reason });
|
|
3193
|
+
this._scheduleReconnect();
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
_onError(error) {
|
|
3197
|
+
console.error('[WebSocket] Error:', error);
|
|
3198
|
+
this._emit('error', { error });
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
_scheduleReconnect() {
|
|
3202
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
3203
|
+
console.log('[WebSocket] Max reconnect attempts reached');
|
|
3204
|
+
this._emit('reconnect_failed', {});
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
this.reconnectAttempts++;
|
|
3209
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
3210
|
+
console.log(
|
|
3211
|
+
`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
3212
|
+
);
|
|
3213
|
+
|
|
3214
|
+
setTimeout(() => {
|
|
3215
|
+
this.connect();
|
|
3216
|
+
}, delay);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
_emit(event, data) {
|
|
3220
|
+
if (this._listeners.has(event)) {
|
|
3221
|
+
this._listeners.get(event).forEach((callback) => {
|
|
3222
|
+
try {
|
|
3223
|
+
callback(data);
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
console.error(`[WebSocket] Error in ${event} handler:`, error);
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Mock support for development
|
|
3232
|
+
_startMockResponses() {
|
|
3233
|
+
// Simulate agent typing and responses
|
|
3234
|
+
this._mockInterval = setInterval(() => {
|
|
3235
|
+
// Randomly emit typing or message events for demo
|
|
3236
|
+
const random = Math.random();
|
|
3237
|
+
if (random < 0.1) {
|
|
3238
|
+
this._emit('typing_started', {
|
|
3239
|
+
conversation_id: 'conv_1',
|
|
3240
|
+
user_id: 'agent_1',
|
|
3241
|
+
user_name: 'Sarah',
|
|
3242
|
+
is_agent: true,
|
|
3243
|
+
});
|
|
3244
|
+
|
|
3245
|
+
// Stop typing after 2 seconds
|
|
3246
|
+
setTimeout(() => {
|
|
3247
|
+
this._emit('typing_stopped', {
|
|
3248
|
+
conversation_id: 'conv_1',
|
|
3249
|
+
user_id: 'agent_1',
|
|
3250
|
+
});
|
|
3251
|
+
}, 2000);
|
|
3252
|
+
}
|
|
3253
|
+
}, 10000);
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Simulate receiving a message (for mock mode)
|
|
3258
|
+
*/
|
|
3259
|
+
simulateMessage(conversationId, message) {
|
|
3260
|
+
if (this.mock) {
|
|
3261
|
+
this._emit('message', {
|
|
3262
|
+
conversation_id: conversationId,
|
|
3263
|
+
message: {
|
|
3264
|
+
id: 'msg_' + Date.now(),
|
|
3265
|
+
content: message.content,
|
|
3266
|
+
sender_type: message.sender_type || 'agent',
|
|
3267
|
+
sender_name: message.sender_name || 'Support',
|
|
3268
|
+
sender_avatar: message.sender_avatar || null,
|
|
3269
|
+
created_at: new Date().toISOString(),
|
|
3270
|
+
},
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
/**
|
|
3277
|
+
* MessengerState - State management for the Messenger widget
|
|
3278
|
+
*/
|
|
3279
|
+
class MessengerState {
|
|
3280
|
+
constructor(options = {}) {
|
|
3281
|
+
this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
|
|
3282
|
+
this.isOpen = false;
|
|
3283
|
+
this.unreadCount = 0;
|
|
3284
|
+
this.activeConversationId = null;
|
|
3285
|
+
|
|
3286
|
+
// Conversations
|
|
3287
|
+
this.conversations = [];
|
|
3288
|
+
this.messages = {}; // { conversationId: [messages] }
|
|
3289
|
+
|
|
3290
|
+
// Help articles
|
|
3291
|
+
this.helpArticles = [];
|
|
3292
|
+
this.helpSearchQuery = '';
|
|
3293
|
+
|
|
3294
|
+
// Changelog
|
|
3295
|
+
this.homeChangelogItems = [];
|
|
3296
|
+
this.changelogItems = [];
|
|
3297
|
+
|
|
3298
|
+
// Team info
|
|
3299
|
+
this.teamName = options.teamName || 'Support';
|
|
3300
|
+
this.teamAvatars = options.teamAvatars || [];
|
|
3301
|
+
this.welcomeMessage = options.welcomeMessage || 'How can we help?';
|
|
3302
|
+
|
|
3303
|
+
// User info
|
|
3304
|
+
this.userContext = options.userContext || null;
|
|
3305
|
+
|
|
3306
|
+
// Feature flags
|
|
3307
|
+
this.enableHelp = options.enableHelp !== false;
|
|
3308
|
+
this.enableChangelog = options.enableChangelog !== false;
|
|
3309
|
+
|
|
3310
|
+
// Agent availability
|
|
3311
|
+
this.agentsOnline = false;
|
|
3312
|
+
this.onlineCount = 0;
|
|
3313
|
+
this.responseTime = 'Usually replies within a few minutes';
|
|
3314
|
+
|
|
3315
|
+
// Typing indicators
|
|
3316
|
+
this.typingUsers = {}; // { conversationId: { userName, timestamp } }
|
|
3317
|
+
|
|
3318
|
+
// Loading states
|
|
3319
|
+
this.isLoading = false;
|
|
3320
|
+
this.isLoadingMessages = false;
|
|
3321
|
+
|
|
3322
|
+
// Listeners
|
|
3323
|
+
this._listeners = new Set();
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
* Subscribe to state changes
|
|
3328
|
+
*/
|
|
3329
|
+
subscribe(callback) {
|
|
3330
|
+
this._listeners.add(callback);
|
|
3331
|
+
return () => this._listeners.delete(callback);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
/**
|
|
3335
|
+
* Notify all listeners of state change
|
|
3336
|
+
*/
|
|
3337
|
+
_notify(changeType, data) {
|
|
3338
|
+
this._listeners.forEach((cb) => cb(changeType, data, this));
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
/**
|
|
3342
|
+
* Set current view
|
|
3343
|
+
*/
|
|
3344
|
+
setView(view) {
|
|
3345
|
+
const previousView = this.currentView;
|
|
3346
|
+
this.currentView = view;
|
|
3347
|
+
this._notify('viewChange', { previousView, currentView: view });
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* Toggle panel open/closed
|
|
3352
|
+
*/
|
|
3353
|
+
setOpen(isOpen) {
|
|
3354
|
+
this.isOpen = isOpen;
|
|
3355
|
+
this._notify('openChange', { isOpen });
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
/**
|
|
3359
|
+
* Set active conversation for chat view
|
|
3360
|
+
*/
|
|
3361
|
+
setActiveConversation(conversationId) {
|
|
3362
|
+
this.activeConversationId = conversationId;
|
|
3363
|
+
this._notify('conversationChange', { conversationId });
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
/**
|
|
3367
|
+
* Update conversations list
|
|
3368
|
+
*/
|
|
3369
|
+
setConversations(conversations) {
|
|
3370
|
+
this.conversations = conversations;
|
|
3371
|
+
this._updateUnreadCount();
|
|
3372
|
+
this._notify('conversationsUpdate', { conversations });
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* Add a new conversation
|
|
3377
|
+
*/
|
|
3378
|
+
addConversation(conversation) {
|
|
3379
|
+
this.conversations.unshift(conversation);
|
|
3380
|
+
this._updateUnreadCount();
|
|
3381
|
+
this._notify('conversationAdded', { conversation });
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
/**
|
|
3385
|
+
* Update messages for a conversation
|
|
3386
|
+
*/
|
|
3387
|
+
setMessages(conversationId, messages) {
|
|
3388
|
+
this.messages[conversationId] = messages;
|
|
3389
|
+
this._notify('messagesUpdate', { conversationId, messages });
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
/**
|
|
3393
|
+
* Add a message to a conversation
|
|
3394
|
+
*/
|
|
3395
|
+
addMessage(conversationId, message) {
|
|
3396
|
+
if (!this.messages[conversationId]) {
|
|
3397
|
+
this.messages[conversationId] = [];
|
|
3398
|
+
}
|
|
3399
|
+
this.messages[conversationId].push(message);
|
|
3400
|
+
|
|
3401
|
+
// Update conversation preview
|
|
3402
|
+
const conv = this.conversations.find((c) => c.id === conversationId);
|
|
3403
|
+
if (conv) {
|
|
3404
|
+
conv.lastMessage = message.content;
|
|
3405
|
+
conv.lastMessageTime = message.timestamp;
|
|
3103
3406
|
if (!message.isOwn) {
|
|
3104
3407
|
conv.unread = (conv.unread || 0) + 1;
|
|
3105
3408
|
this._updateUnreadCount();
|
|
@@ -3828,7 +4131,10 @@
|
|
|
3828
4131
|
data.conversationId === this.state.activeConversationId
|
|
3829
4132
|
) {
|
|
3830
4133
|
this._hideTypingIndicator();
|
|
3831
|
-
} else if (
|
|
4134
|
+
} else if (
|
|
4135
|
+
type === 'messagesUpdate' &&
|
|
4136
|
+
data.conversationId === this.state.activeConversationId
|
|
4137
|
+
) {
|
|
3832
4138
|
this._updateContent();
|
|
3833
4139
|
}
|
|
3834
4140
|
});
|
|
@@ -3888,14 +4194,17 @@
|
|
|
3888
4194
|
</div>
|
|
3889
4195
|
`;
|
|
3890
4196
|
|
|
3891
|
-
this._typingIndicator = this.element.querySelector(
|
|
4197
|
+
this._typingIndicator = this.element.querySelector(
|
|
4198
|
+
'.messenger-typing-indicator'
|
|
4199
|
+
);
|
|
3892
4200
|
this._attachEvents();
|
|
3893
4201
|
this._scrollToBottom();
|
|
3894
4202
|
}
|
|
3895
4203
|
|
|
3896
4204
|
_renderEmptyState(isNewConversation = false) {
|
|
3897
4205
|
const avatarHtml = this._renderTeamAvatars();
|
|
3898
|
-
const responseTime =
|
|
4206
|
+
const responseTime =
|
|
4207
|
+
this.state.responseTime || 'We typically reply within a few minutes';
|
|
3899
4208
|
const isOnline = this.state.agentsOnline;
|
|
3900
4209
|
|
|
3901
4210
|
return `
|
|
@@ -4151,7 +4460,9 @@
|
|
|
4151
4460
|
_showTypingIndicator(userName) {
|
|
4152
4461
|
if (this._typingIndicator) {
|
|
4153
4462
|
this._typingIndicator.style.display = 'flex';
|
|
4154
|
-
const textEl = this._typingIndicator.querySelector(
|
|
4463
|
+
const textEl = this._typingIndicator.querySelector(
|
|
4464
|
+
'.messenger-typing-text'
|
|
4465
|
+
);
|
|
4155
4466
|
if (textEl) {
|
|
4156
4467
|
textEl.textContent = `${userName || 'Support'} is typing...`;
|
|
4157
4468
|
}
|
|
@@ -4532,255 +4843,19 @@
|
|
|
4532
4843
|
<p>Try a different search term</p>
|
|
4533
4844
|
</div>
|
|
4534
4845
|
`;
|
|
4535
|
-
}
|
|
4536
|
-
|
|
4537
|
-
return `
|
|
4538
|
-
<div class="messenger-help-empty">
|
|
4539
|
-
<div class="messenger-help-empty-icon">
|
|
4540
|
-
<i class="ph ph-question" style="font-size: 48px;"></i>
|
|
4541
|
-
</div>
|
|
4542
|
-
<h3>Help collections</h3>
|
|
4543
|
-
<p>No collections available yet</p>
|
|
4544
|
-
</div>
|
|
4545
|
-
`;
|
|
4546
|
-
}
|
|
4547
|
-
|
|
4548
|
-
_attachEvents() {
|
|
4549
|
-
// Close button
|
|
4550
|
-
this.element
|
|
4551
|
-
.querySelector('.messenger-close-btn')
|
|
4552
|
-
.addEventListener('click', () => {
|
|
4553
|
-
this.state.setOpen(false);
|
|
4554
|
-
});
|
|
4555
|
-
|
|
4556
|
-
// Search input
|
|
4557
|
-
const searchInput = this.element.querySelector(
|
|
4558
|
-
'.messenger-help-search-input'
|
|
4559
|
-
);
|
|
4560
|
-
let searchTimeout;
|
|
4561
|
-
searchInput.addEventListener('input', (e) => {
|
|
4562
|
-
clearTimeout(searchTimeout);
|
|
4563
|
-
searchTimeout = setTimeout(() => {
|
|
4564
|
-
this.state.setHelpSearchQuery(e.target.value);
|
|
4565
|
-
}, 300);
|
|
4566
|
-
});
|
|
4567
|
-
|
|
4568
|
-
this._attachCollectionEvents();
|
|
4569
|
-
}
|
|
4570
|
-
|
|
4571
|
-
_attachCollectionEvents() {
|
|
4572
|
-
this.element
|
|
4573
|
-
.querySelectorAll('.messenger-help-collection')
|
|
4574
|
-
.forEach((item) => {
|
|
4575
|
-
item.addEventListener('click', () => {
|
|
4576
|
-
const collectionId = item.dataset.collectionId;
|
|
4577
|
-
const collection = this.state.helpArticles.find(
|
|
4578
|
-
(c) => c.id === collectionId
|
|
4579
|
-
);
|
|
4580
|
-
if (collection && collection.url) {
|
|
4581
|
-
window.open(collection.url, '_blank');
|
|
4582
|
-
} else if (this.options.onArticleClick) {
|
|
4583
|
-
this.options.onArticleClick(collection);
|
|
4584
|
-
}
|
|
4585
|
-
});
|
|
4586
|
-
});
|
|
4587
|
-
}
|
|
4588
|
-
|
|
4589
|
-
destroy() {
|
|
4590
|
-
if (this._unsubscribe) {
|
|
4591
|
-
this._unsubscribe();
|
|
4592
|
-
}
|
|
4593
|
-
if (this.element && this.element.parentNode) {
|
|
4594
|
-
this.element.parentNode.removeChild(this.element);
|
|
4595
|
-
}
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
|
|
4599
|
-
/**
|
|
4600
|
-
* HomeView - Welcome screen with team info and quick actions
|
|
4601
|
-
*/
|
|
4602
|
-
class HomeView {
|
|
4603
|
-
constructor(state, options = {}) {
|
|
4604
|
-
this.state = state;
|
|
4605
|
-
this.options = options;
|
|
4606
|
-
this.element = null;
|
|
4607
|
-
this._unsubscribe = null;
|
|
4608
|
-
}
|
|
4609
|
-
|
|
4610
|
-
render() {
|
|
4611
|
-
this.element = document.createElement('div');
|
|
4612
|
-
this.element.className = 'messenger-view messenger-home-view';
|
|
4613
|
-
|
|
4614
|
-
this._updateContent();
|
|
4615
|
-
|
|
4616
|
-
// Subscribe to state changes to re-render when data loads
|
|
4617
|
-
this._unsubscribe = this.state.subscribe((type) => {
|
|
4618
|
-
if (type === 'homeChangelogUpdate' || type === 'conversationsUpdate' || type === 'availabilityUpdate') {
|
|
4619
|
-
this._updateContent();
|
|
4620
|
-
}
|
|
4621
|
-
});
|
|
4622
|
-
|
|
4623
|
-
return this.element;
|
|
4624
|
-
}
|
|
4625
|
-
|
|
4626
|
-
_updateContent() {
|
|
4627
|
-
const avatarsHtml = this._renderAvatarStack();
|
|
4628
|
-
const recentChangelogHtml = this._renderRecentChangelog();
|
|
4629
|
-
|
|
4630
|
-
this.element.innerHTML = `
|
|
4631
|
-
<div class="messenger-home-header">
|
|
4632
|
-
<div class="messenger-home-header-top">
|
|
4633
|
-
<div class="messenger-home-logo">
|
|
4634
|
-
${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
|
|
4635
|
-
</div>
|
|
4636
|
-
<div class="messenger-home-avatars">${avatarsHtml}</div>
|
|
4637
|
-
<button class="messenger-close-btn" aria-label="Close">
|
|
4638
|
-
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
4639
|
-
</button>
|
|
4640
|
-
</div>
|
|
4641
|
-
<div class="messenger-home-welcome">
|
|
4642
|
-
<span class="messenger-home-greeting">Hello there.</span>
|
|
4643
|
-
<span class="messenger-home-question">${this.state.welcomeMessage}</span>
|
|
4644
|
-
${this._renderAvailabilityStatus()}
|
|
4645
|
-
</div>
|
|
4646
|
-
</div>
|
|
4647
|
-
|
|
4648
|
-
<div class="messenger-home-body">
|
|
4649
|
-
<button class="messenger-home-message-btn">
|
|
4650
|
-
<span>Send us a message</span>
|
|
4651
|
-
<i class="ph ph-arrow-right" style="font-size: 16px;"></i>
|
|
4652
|
-
</button>
|
|
4653
|
-
|
|
4654
|
-
${this._renderFeaturedCard()}
|
|
4655
|
-
|
|
4656
|
-
${recentChangelogHtml}
|
|
4657
|
-
</div>
|
|
4658
|
-
`;
|
|
4659
|
-
|
|
4660
|
-
this._attachEvents();
|
|
4661
|
-
}
|
|
4662
|
-
|
|
4663
|
-
_renderAvatarStack() {
|
|
4664
|
-
const avatars = this.state.teamAvatars;
|
|
4665
|
-
if (!avatars || avatars.length === 0) {
|
|
4666
|
-
// Default avatars with initials
|
|
4667
|
-
return `
|
|
4668
|
-
<div class="messenger-avatar-stack">
|
|
4669
|
-
<div class="messenger-avatar" style="background: #5856d6;">S</div>
|
|
4670
|
-
<div class="messenger-avatar" style="background: #007aff;">T</div>
|
|
4671
|
-
</div>
|
|
4672
|
-
`;
|
|
4673
|
-
}
|
|
4674
|
-
|
|
4675
|
-
const avatarItems = avatars
|
|
4676
|
-
.slice(0, 3)
|
|
4677
|
-
.map((avatar, i) => {
|
|
4678
|
-
if (typeof avatar === 'string' && avatar.startsWith('http')) {
|
|
4679
|
-
return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
|
|
4680
|
-
}
|
|
4681
|
-
return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
|
|
4682
|
-
})
|
|
4683
|
-
.join('');
|
|
4684
|
-
|
|
4685
|
-
return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
|
|
4686
|
-
}
|
|
4687
|
-
|
|
4688
|
-
_getAvatarColor(index) {
|
|
4689
|
-
const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
|
|
4690
|
-
return colors[index % colors.length];
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
_renderAvailabilityStatus() {
|
|
4694
|
-
const isOnline = this.state.agentsOnline;
|
|
4695
|
-
const responseTime = this.state.responseTime || 'We typically reply within a few minutes';
|
|
4696
|
-
|
|
4697
|
-
if (isOnline) {
|
|
4698
|
-
return `
|
|
4699
|
-
<div class="messenger-home-availability">
|
|
4700
|
-
<span class="messenger-availability-dot messenger-availability-online"></span>
|
|
4701
|
-
<span class="messenger-availability-text">We're online now</span>
|
|
4702
|
-
</div>
|
|
4703
|
-
`;
|
|
4704
|
-
}
|
|
4705
|
-
|
|
4706
|
-
return `
|
|
4707
|
-
<div class="messenger-home-availability">
|
|
4708
|
-
<span class="messenger-availability-dot messenger-availability-away"></span>
|
|
4709
|
-
<span class="messenger-availability-text">${responseTime}</span>
|
|
4710
|
-
</div>
|
|
4711
|
-
`;
|
|
4712
|
-
}
|
|
4713
|
-
|
|
4714
|
-
_renderFeaturedCard() {
|
|
4715
|
-
// Only show if there's featured content configured
|
|
4716
|
-
if (!this.options.featuredContent) {
|
|
4717
|
-
return '';
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
|
-
const { title, description, imageUrl, action } =
|
|
4721
|
-
this.options.featuredContent;
|
|
4722
|
-
|
|
4723
|
-
return `
|
|
4724
|
-
<div class="messenger-home-featured">
|
|
4725
|
-
${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
|
|
4726
|
-
<div class="messenger-home-featured-content">
|
|
4727
|
-
<h3>${title}</h3>
|
|
4728
|
-
<p>${description}</p>
|
|
4729
|
-
</div>
|
|
4730
|
-
${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
|
|
4731
|
-
</div>
|
|
4732
|
-
`;
|
|
4733
|
-
}
|
|
4734
|
-
|
|
4735
|
-
_renderRecentChangelog() {
|
|
4736
|
-
// Show recent changelog preview as cards with images
|
|
4737
|
-
const changelogItems = this.state.homeChangelogItems;
|
|
4738
|
-
if (changelogItems.length === 0) {
|
|
4739
|
-
return '';
|
|
4740
|
-
}
|
|
4741
|
-
|
|
4742
|
-
const changelogHtml = changelogItems
|
|
4743
|
-
.map(
|
|
4744
|
-
(item) => `
|
|
4745
|
-
<div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
|
|
4746
|
-
${
|
|
4747
|
-
item.coverImage
|
|
4748
|
-
? `
|
|
4749
|
-
<div class="messenger-home-changelog-cover">
|
|
4750
|
-
<img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
|
|
4751
|
-
${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
|
|
4752
|
-
</div>
|
|
4753
|
-
`
|
|
4754
|
-
: ''
|
|
4755
|
-
}
|
|
4756
|
-
<div class="messenger-home-changelog-card-content">
|
|
4757
|
-
<h4 class="messenger-home-changelog-card-title">${item.title}</h4>
|
|
4758
|
-
<p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
|
|
4759
|
-
</div>
|
|
4760
|
-
</div>
|
|
4761
|
-
`
|
|
4762
|
-
)
|
|
4763
|
-
.join('');
|
|
4846
|
+
}
|
|
4764
4847
|
|
|
4765
4848
|
return `
|
|
4766
|
-
<div class="messenger-
|
|
4767
|
-
|
|
4849
|
+
<div class="messenger-help-empty">
|
|
4850
|
+
<div class="messenger-help-empty-icon">
|
|
4851
|
+
<i class="ph ph-question" style="font-size: 48px;"></i>
|
|
4852
|
+
</div>
|
|
4853
|
+
<h3>Help collections</h3>
|
|
4854
|
+
<p>No collections available yet</p>
|
|
4768
4855
|
</div>
|
|
4769
4856
|
`;
|
|
4770
4857
|
}
|
|
4771
4858
|
|
|
4772
|
-
_formatDate(dateString) {
|
|
4773
|
-
if (!dateString) return '';
|
|
4774
|
-
const date = new Date(dateString);
|
|
4775
|
-
const now = new Date();
|
|
4776
|
-
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
|
4777
|
-
|
|
4778
|
-
if (diffDays === 0) return 'Today';
|
|
4779
|
-
if (diffDays === 1) return 'Yesterday';
|
|
4780
|
-
if (diffDays < 7) return `${diffDays}d ago`;
|
|
4781
|
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
4782
|
-
}
|
|
4783
|
-
|
|
4784
4859
|
_attachEvents() {
|
|
4785
4860
|
// Close button
|
|
4786
4861
|
this.element
|
|
@@ -4789,48 +4864,37 @@
|
|
|
4789
4864
|
this.state.setOpen(false);
|
|
4790
4865
|
});
|
|
4791
4866
|
|
|
4792
|
-
//
|
|
4793
|
-
this.element
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4867
|
+
// Search input
|
|
4868
|
+
const searchInput = this.element.querySelector(
|
|
4869
|
+
'.messenger-help-search-input'
|
|
4870
|
+
);
|
|
4871
|
+
let searchTimeout;
|
|
4872
|
+
searchInput.addEventListener('input', (e) => {
|
|
4873
|
+
clearTimeout(searchTimeout);
|
|
4874
|
+
searchTimeout = setTimeout(() => {
|
|
4875
|
+
this.state.setHelpSearchQuery(e.target.value);
|
|
4876
|
+
}, 300);
|
|
4877
|
+
});
|
|
4798
4878
|
|
|
4799
|
-
|
|
4879
|
+
this._attachCollectionEvents();
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
_attachCollectionEvents() {
|
|
4800
4883
|
this.element
|
|
4801
|
-
.querySelectorAll('.messenger-
|
|
4884
|
+
.querySelectorAll('.messenger-help-collection')
|
|
4802
4885
|
.forEach((item) => {
|
|
4803
4886
|
item.addEventListener('click', () => {
|
|
4804
|
-
|
|
4805
|
-
this.state.
|
|
4887
|
+
const collectionId = item.dataset.collectionId;
|
|
4888
|
+
const collection = this.state.helpArticles.find(
|
|
4889
|
+
(c) => c.id === collectionId
|
|
4890
|
+
);
|
|
4891
|
+
if (collection && collection.url) {
|
|
4892
|
+
window.open(collection.url, '_blank');
|
|
4893
|
+
} else if (this.options.onArticleClick) {
|
|
4894
|
+
this.options.onArticleClick(collection);
|
|
4895
|
+
}
|
|
4806
4896
|
});
|
|
4807
4897
|
});
|
|
4808
|
-
|
|
4809
|
-
// See all changelog
|
|
4810
|
-
const seeAllBtn = this.element.querySelector(
|
|
4811
|
-
'.messenger-home-changelog-all'
|
|
4812
|
-
);
|
|
4813
|
-
if (seeAllBtn) {
|
|
4814
|
-
seeAllBtn.addEventListener('click', () => {
|
|
4815
|
-
this.state.setView('changelog');
|
|
4816
|
-
});
|
|
4817
|
-
}
|
|
4818
|
-
|
|
4819
|
-
// Featured card action
|
|
4820
|
-
const featuredBtn = this.element.querySelector(
|
|
4821
|
-
'.messenger-home-featured-btn'
|
|
4822
|
-
);
|
|
4823
|
-
if (featuredBtn) {
|
|
4824
|
-
featuredBtn.addEventListener('click', () => {
|
|
4825
|
-
const action = featuredBtn.dataset.action;
|
|
4826
|
-
const value = featuredBtn.dataset.value;
|
|
4827
|
-
if (action === 'url') {
|
|
4828
|
-
window.open(value, '_blank');
|
|
4829
|
-
} else if (action === 'view') {
|
|
4830
|
-
this.state.setView(value);
|
|
4831
|
-
}
|
|
4832
|
-
});
|
|
4833
|
-
}
|
|
4834
4898
|
}
|
|
4835
4899
|
|
|
4836
4900
|
destroy() {
|
|
@@ -4844,277 +4908,255 @@
|
|
|
4844
4908
|
}
|
|
4845
4909
|
|
|
4846
4910
|
/**
|
|
4847
|
-
*
|
|
4911
|
+
* HomeView - Welcome screen with team info and quick actions
|
|
4848
4912
|
*/
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
this.
|
|
4853
|
-
this.
|
|
4854
|
-
this.
|
|
4855
|
-
this.mock = config.mock || false;
|
|
4856
|
-
|
|
4857
|
-
this.ws = null;
|
|
4858
|
-
this.reconnectAttempts = 0;
|
|
4859
|
-
this.maxReconnectAttempts = 5;
|
|
4860
|
-
this.reconnectDelay = 1000;
|
|
4861
|
-
this.pingInterval = null;
|
|
4862
|
-
this.isConnected = false;
|
|
4863
|
-
|
|
4864
|
-
// Event listeners
|
|
4865
|
-
this._listeners = new Map();
|
|
4866
|
-
|
|
4867
|
-
// Bind methods
|
|
4868
|
-
this._onOpen = this._onOpen.bind(this);
|
|
4869
|
-
this._onMessage = this._onMessage.bind(this);
|
|
4870
|
-
this._onClose = this._onClose.bind(this);
|
|
4871
|
-
this._onError = this._onError.bind(this);
|
|
4913
|
+
class HomeView {
|
|
4914
|
+
constructor(state, options = {}) {
|
|
4915
|
+
this.state = state;
|
|
4916
|
+
this.options = options;
|
|
4917
|
+
this.element = null;
|
|
4918
|
+
this._unsubscribe = null;
|
|
4872
4919
|
}
|
|
4873
4920
|
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
connect(sessionToken = null) {
|
|
4878
|
-
if (sessionToken) {
|
|
4879
|
-
this.sessionToken = sessionToken;
|
|
4880
|
-
}
|
|
4881
|
-
|
|
4882
|
-
if (!this.sessionToken) {
|
|
4883
|
-
console.warn('[WebSocket] No session token provided');
|
|
4884
|
-
return;
|
|
4885
|
-
}
|
|
4921
|
+
render() {
|
|
4922
|
+
this.element = document.createElement('div');
|
|
4923
|
+
this.element.className = 'messenger-view messenger-home-view';
|
|
4886
4924
|
|
|
4887
|
-
|
|
4888
|
-
if (this.mock) {
|
|
4889
|
-
this.isConnected = true;
|
|
4890
|
-
this._emit('connected', {});
|
|
4891
|
-
this._startMockResponses();
|
|
4892
|
-
return;
|
|
4893
|
-
}
|
|
4925
|
+
this._updateContent();
|
|
4894
4926
|
|
|
4895
|
-
//
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4927
|
+
// Subscribe to state changes to re-render when data loads
|
|
4928
|
+
this._unsubscribe = this.state.subscribe((type) => {
|
|
4929
|
+
if (
|
|
4930
|
+
type === 'homeChangelogUpdate' ||
|
|
4931
|
+
type === 'conversationsUpdate' ||
|
|
4932
|
+
type === 'availabilityUpdate'
|
|
4933
|
+
) {
|
|
4934
|
+
this._updateContent();
|
|
4935
|
+
}
|
|
4936
|
+
});
|
|
4900
4937
|
|
|
4901
|
-
|
|
4902
|
-
this.ws = new WebSocket(wsURL);
|
|
4903
|
-
this.ws.onopen = this._onOpen;
|
|
4904
|
-
this.ws.onmessage = this._onMessage;
|
|
4905
|
-
this.ws.onclose = this._onClose;
|
|
4906
|
-
this.ws.onerror = this._onError;
|
|
4907
|
-
} catch (error) {
|
|
4908
|
-
console.error('[WebSocket] Connection error:', error);
|
|
4909
|
-
this._scheduleReconnect();
|
|
4910
|
-
}
|
|
4938
|
+
return this.element;
|
|
4911
4939
|
}
|
|
4912
4940
|
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
disconnect() {
|
|
4917
|
-
this.isConnected = false;
|
|
4918
|
-
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
4941
|
+
_updateContent() {
|
|
4942
|
+
const avatarsHtml = this._renderAvatarStack();
|
|
4943
|
+
const recentChangelogHtml = this._renderRecentChangelog();
|
|
4919
4944
|
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4945
|
+
this.element.innerHTML = `
|
|
4946
|
+
<div class="messenger-home-header">
|
|
4947
|
+
<div class="messenger-home-header-top">
|
|
4948
|
+
<div class="messenger-home-logo">
|
|
4949
|
+
${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
|
|
4950
|
+
</div>
|
|
4951
|
+
<div class="messenger-home-avatars">${avatarsHtml}</div>
|
|
4952
|
+
<button class="messenger-close-btn" aria-label="Close">
|
|
4953
|
+
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
4954
|
+
</button>
|
|
4955
|
+
</div>
|
|
4956
|
+
<div class="messenger-home-welcome">
|
|
4957
|
+
<span class="messenger-home-greeting">Hello there.</span>
|
|
4958
|
+
<span class="messenger-home-question">${this.state.welcomeMessage}</span>
|
|
4959
|
+
${this._renderAvailabilityStatus()}
|
|
4960
|
+
</div>
|
|
4961
|
+
</div>
|
|
4924
4962
|
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4963
|
+
<div class="messenger-home-body">
|
|
4964
|
+
<button class="messenger-home-message-btn">
|
|
4965
|
+
<span>Send us a message</span>
|
|
4966
|
+
<i class="ph ph-arrow-right" style="font-size: 16px;"></i>
|
|
4967
|
+
</button>
|
|
4929
4968
|
|
|
4930
|
-
|
|
4931
|
-
clearInterval(this._mockInterval);
|
|
4932
|
-
this._mockInterval = null;
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4969
|
+
${this._renderFeaturedCard()}
|
|
4935
4970
|
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
* @param {Function} callback - Event handler
|
|
4940
|
-
* @returns {Function} Unsubscribe function
|
|
4941
|
-
*/
|
|
4942
|
-
on(event, callback) {
|
|
4943
|
-
if (!this._listeners.has(event)) {
|
|
4944
|
-
this._listeners.set(event, new Set());
|
|
4945
|
-
}
|
|
4946
|
-
this._listeners.get(event).add(callback);
|
|
4947
|
-
return () => this._listeners.get(event).delete(callback);
|
|
4948
|
-
}
|
|
4971
|
+
${recentChangelogHtml}
|
|
4972
|
+
</div>
|
|
4973
|
+
`;
|
|
4949
4974
|
|
|
4950
|
-
|
|
4951
|
-
* Remove event listener
|
|
4952
|
-
*/
|
|
4953
|
-
off(event, callback) {
|
|
4954
|
-
if (this._listeners.has(event)) {
|
|
4955
|
-
this._listeners.get(event).delete(callback);
|
|
4956
|
-
}
|
|
4975
|
+
this._attachEvents();
|
|
4957
4976
|
}
|
|
4958
4977
|
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4978
|
+
_renderAvatarStack() {
|
|
4979
|
+
const avatars = this.state.teamAvatars;
|
|
4980
|
+
if (!avatars || avatars.length === 0) {
|
|
4981
|
+
// Default avatars with initials
|
|
4982
|
+
return `
|
|
4983
|
+
<div class="messenger-avatar-stack">
|
|
4984
|
+
<div class="messenger-avatar" style="background: #5856d6;">S</div>
|
|
4985
|
+
<div class="messenger-avatar" style="background: #007aff;">T</div>
|
|
4986
|
+
</div>
|
|
4987
|
+
`;
|
|
4966
4988
|
}
|
|
4967
4989
|
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4990
|
+
const avatarItems = avatars
|
|
4991
|
+
.slice(0, 3)
|
|
4992
|
+
.map((avatar, i) => {
|
|
4993
|
+
if (typeof avatar === 'string' && avatar.startsWith('http')) {
|
|
4994
|
+
return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
|
|
4995
|
+
}
|
|
4996
|
+
return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
|
|
4997
|
+
})
|
|
4998
|
+
.join('');
|
|
4973
4999
|
|
|
4974
|
-
|
|
4975
|
-
this.ws.send(JSON.stringify({ type, payload }));
|
|
4976
|
-
}
|
|
5000
|
+
return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
|
|
4977
5001
|
}
|
|
4978
5002
|
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
console.log('[WebSocket] Connected');
|
|
4983
|
-
this.isConnected = true;
|
|
4984
|
-
this.reconnectAttempts = 0;
|
|
4985
|
-
this._emit('connected', {});
|
|
4986
|
-
|
|
4987
|
-
// Start ping interval to keep connection alive
|
|
4988
|
-
this.pingInterval = setInterval(() => {
|
|
4989
|
-
this.send('ping', {});
|
|
4990
|
-
}, 30000);
|
|
5003
|
+
_getAvatarColor(index) {
|
|
5004
|
+
const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
|
|
5005
|
+
return colors[index % colors.length];
|
|
4991
5006
|
}
|
|
4992
5007
|
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5008
|
+
_renderAvailabilityStatus() {
|
|
5009
|
+
const isOnline = this.state.agentsOnline;
|
|
5010
|
+
const responseTime =
|
|
5011
|
+
this.state.responseTime || 'We typically reply within a few minutes';
|
|
4997
5012
|
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
break;
|
|
5006
|
-
case 'typing:stopped':
|
|
5007
|
-
this._emit('typing_stopped', payload);
|
|
5008
|
-
break;
|
|
5009
|
-
case 'conversation:updated':
|
|
5010
|
-
this._emit('conversation_updated', payload);
|
|
5011
|
-
break;
|
|
5012
|
-
case 'conversation:closed':
|
|
5013
|
-
this._emit('conversation_closed', payload);
|
|
5014
|
-
break;
|
|
5015
|
-
case 'availability:changed':
|
|
5016
|
-
this._emit('availability_changed', payload);
|
|
5017
|
-
break;
|
|
5018
|
-
case 'pong':
|
|
5019
|
-
// Ping response, ignore
|
|
5020
|
-
break;
|
|
5021
|
-
default:
|
|
5022
|
-
console.log('[WebSocket] Unknown event:', type, payload);
|
|
5023
|
-
}
|
|
5024
|
-
} catch (error) {
|
|
5025
|
-
console.error('[WebSocket] Failed to parse message:', error);
|
|
5013
|
+
if (isOnline) {
|
|
5014
|
+
return `
|
|
5015
|
+
<div class="messenger-home-availability">
|
|
5016
|
+
<span class="messenger-availability-dot messenger-availability-online"></span>
|
|
5017
|
+
<span class="messenger-availability-text">We're online now</span>
|
|
5018
|
+
</div>
|
|
5019
|
+
`;
|
|
5026
5020
|
}
|
|
5027
|
-
}
|
|
5028
5021
|
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5022
|
+
return `
|
|
5023
|
+
<div class="messenger-home-availability">
|
|
5024
|
+
<span class="messenger-availability-dot messenger-availability-away"></span>
|
|
5025
|
+
<span class="messenger-availability-text">${responseTime}</span>
|
|
5026
|
+
</div>
|
|
5027
|
+
`;
|
|
5028
|
+
}
|
|
5032
5029
|
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5030
|
+
_renderFeaturedCard() {
|
|
5031
|
+
// Only show if there's featured content configured
|
|
5032
|
+
if (!this.options.featuredContent) {
|
|
5033
|
+
return '';
|
|
5036
5034
|
}
|
|
5037
5035
|
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
}
|
|
5036
|
+
const { title, description, imageUrl, action } =
|
|
5037
|
+
this.options.featuredContent;
|
|
5041
5038
|
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5039
|
+
return `
|
|
5040
|
+
<div class="messenger-home-featured">
|
|
5041
|
+
${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
|
|
5042
|
+
<div class="messenger-home-featured-content">
|
|
5043
|
+
<h3>${title}</h3>
|
|
5044
|
+
<p>${description}</p>
|
|
5045
|
+
</div>
|
|
5046
|
+
${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
|
|
5047
|
+
</div>
|
|
5048
|
+
`;
|
|
5045
5049
|
}
|
|
5046
5050
|
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
return;
|
|
5051
|
+
_renderRecentChangelog() {
|
|
5052
|
+
// Show recent changelog preview as cards with images
|
|
5053
|
+
const changelogItems = this.state.homeChangelogItems;
|
|
5054
|
+
if (changelogItems.length === 0) {
|
|
5055
|
+
return '';
|
|
5052
5056
|
}
|
|
5053
5057
|
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5058
|
+
const changelogHtml = changelogItems
|
|
5059
|
+
.map(
|
|
5060
|
+
(item) => `
|
|
5061
|
+
<div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
|
|
5062
|
+
${
|
|
5063
|
+
item.coverImage
|
|
5064
|
+
? `
|
|
5065
|
+
<div class="messenger-home-changelog-cover">
|
|
5066
|
+
<img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
|
|
5067
|
+
${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
|
|
5068
|
+
</div>
|
|
5069
|
+
`
|
|
5070
|
+
: ''
|
|
5071
|
+
}
|
|
5072
|
+
<div class="messenger-home-changelog-card-content">
|
|
5073
|
+
<h4 class="messenger-home-changelog-card-title">${item.title}</h4>
|
|
5074
|
+
<p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
|
|
5075
|
+
</div>
|
|
5076
|
+
</div>
|
|
5077
|
+
`
|
|
5078
|
+
)
|
|
5079
|
+
.join('');
|
|
5057
5080
|
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5081
|
+
return `
|
|
5082
|
+
<div class="messenger-home-changelog-section">
|
|
5083
|
+
${changelogHtml}
|
|
5084
|
+
</div>
|
|
5085
|
+
`;
|
|
5061
5086
|
}
|
|
5062
5087
|
|
|
5063
|
-
|
|
5064
|
-
if (
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
}
|
|
5088
|
+
_formatDate(dateString) {
|
|
5089
|
+
if (!dateString) return '';
|
|
5090
|
+
const date = new Date(dateString);
|
|
5091
|
+
const now = new Date();
|
|
5092
|
+
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
|
5093
|
+
|
|
5094
|
+
if (diffDays === 0) return 'Today';
|
|
5095
|
+
if (diffDays === 1) return 'Yesterday';
|
|
5096
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
5097
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
5073
5098
|
}
|
|
5074
5099
|
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5100
|
+
_attachEvents() {
|
|
5101
|
+
// Close button
|
|
5102
|
+
this.element
|
|
5103
|
+
.querySelector('.messenger-close-btn')
|
|
5104
|
+
.addEventListener('click', () => {
|
|
5105
|
+
this.state.setOpen(false);
|
|
5106
|
+
});
|
|
5107
|
+
|
|
5108
|
+
// Send message button
|
|
5109
|
+
this.element
|
|
5110
|
+
.querySelector('.messenger-home-message-btn')
|
|
5111
|
+
.addEventListener('click', () => {
|
|
5112
|
+
this.state.setView('messages');
|
|
5113
|
+
});
|
|
5114
|
+
|
|
5115
|
+
// Changelog items
|
|
5116
|
+
this.element
|
|
5117
|
+
.querySelectorAll('.messenger-home-changelog-item')
|
|
5118
|
+
.forEach((item) => {
|
|
5119
|
+
item.addEventListener('click', () => {
|
|
5120
|
+
// Navigate to changelog view with specific item selected
|
|
5121
|
+
this.state.setView('changelog');
|
|
5087
5122
|
});
|
|
5123
|
+
});
|
|
5088
5124
|
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
}
|
|
5097
|
-
}
|
|
5098
|
-
}
|
|
5125
|
+
// See all changelog
|
|
5126
|
+
const seeAllBtn = this.element.querySelector(
|
|
5127
|
+
'.messenger-home-changelog-all'
|
|
5128
|
+
);
|
|
5129
|
+
if (seeAllBtn) {
|
|
5130
|
+
seeAllBtn.addEventListener('click', () => {
|
|
5131
|
+
this.state.setView('changelog');
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5099
5134
|
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
if (
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
created_at: new Date().toISOString(),
|
|
5114
|
-
},
|
|
5135
|
+
// Featured card action
|
|
5136
|
+
const featuredBtn = this.element.querySelector(
|
|
5137
|
+
'.messenger-home-featured-btn'
|
|
5138
|
+
);
|
|
5139
|
+
if (featuredBtn) {
|
|
5140
|
+
featuredBtn.addEventListener('click', () => {
|
|
5141
|
+
const action = featuredBtn.dataset.action;
|
|
5142
|
+
const value = featuredBtn.dataset.value;
|
|
5143
|
+
if (action === 'url') {
|
|
5144
|
+
window.open(value, '_blank');
|
|
5145
|
+
} else if (action === 'view') {
|
|
5146
|
+
this.state.setView(value);
|
|
5147
|
+
}
|
|
5115
5148
|
});
|
|
5116
5149
|
}
|
|
5117
5150
|
}
|
|
5151
|
+
|
|
5152
|
+
destroy() {
|
|
5153
|
+
if (this._unsubscribe) {
|
|
5154
|
+
this._unsubscribe();
|
|
5155
|
+
}
|
|
5156
|
+
if (this.element && this.element.parentNode) {
|
|
5157
|
+
this.element.parentNode.removeChild(this.element);
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5118
5160
|
}
|
|
5119
5161
|
|
|
5120
5162
|
/**
|
|
@@ -5325,7 +5367,10 @@
|
|
|
5325
5367
|
this.messengerState.addMessage(conversation_id, localMessage);
|
|
5326
5368
|
|
|
5327
5369
|
// Update unread count if panel is closed or viewing different conversation
|
|
5328
|
-
if (
|
|
5370
|
+
if (
|
|
5371
|
+
!this.messengerState.isOpen ||
|
|
5372
|
+
this.messengerState.activeConversationId !== conversation_id
|
|
5373
|
+
) {
|
|
5329
5374
|
this._updateUnreadCount();
|
|
5330
5375
|
}
|
|
5331
5376
|
}
|
|
@@ -5359,7 +5404,9 @@
|
|
|
5359
5404
|
const response = await this.apiService.getUnreadCount();
|
|
5360
5405
|
if (response.status && response.data) {
|
|
5361
5406
|
this.messengerState.unreadCount = response.data.unread_count || 0;
|
|
5362
|
-
this.messengerState._notify('unreadCountChange', {
|
|
5407
|
+
this.messengerState._notify('unreadCountChange', {
|
|
5408
|
+
count: this.messengerState.unreadCount,
|
|
5409
|
+
});
|
|
5363
5410
|
}
|
|
5364
5411
|
} catch (error) {
|
|
5365
5412
|
console.error('[MessengerWidget] Failed to get unread count:', error);
|
|
@@ -5533,9 +5580,16 @@
|
|
|
5533
5580
|
// Transform API response to local format
|
|
5534
5581
|
return response.data.map((conv) => ({
|
|
5535
5582
|
id: conv.id,
|
|
5536
|
-
title:
|
|
5583
|
+
title:
|
|
5584
|
+
conv.subject ||
|
|
5585
|
+
`Chat with ${conv.assigned_user?.name || 'Support'}`,
|
|
5537
5586
|
participants: conv.assigned_user
|
|
5538
|
-
? [
|
|
5587
|
+
? [
|
|
5588
|
+
{
|
|
5589
|
+
name: conv.assigned_user.name,
|
|
5590
|
+
avatarUrl: conv.assigned_user.avatar,
|
|
5591
|
+
},
|
|
5592
|
+
]
|
|
5539
5593
|
: [{ name: 'Support', avatarUrl: null }],
|
|
5540
5594
|
lastMessage: conv.preview || conv.snippet || '',
|
|
5541
5595
|
lastMessageTime: conv.last_message_at,
|
|
@@ -5559,7 +5613,8 @@
|
|
|
5559
5613
|
id: collection.id,
|
|
5560
5614
|
title: collection.title || collection.name,
|
|
5561
5615
|
description: collection.description || '',
|
|
5562
|
-
articleCount:
|
|
5616
|
+
articleCount:
|
|
5617
|
+
collection.article_count || collection.articleCount || 0,
|
|
5563
5618
|
icon: collection.icon || 'ph-book-open',
|
|
5564
5619
|
url: collection.url || `#/help/${collection.slug || collection.id}`,
|
|
5565
5620
|
}));
|
|
@@ -5584,7 +5639,9 @@
|
|
|
5584
5639
|
isOwn: msg.sender_type === 'customer',
|
|
5585
5640
|
timestamp: msg.created_at,
|
|
5586
5641
|
sender: {
|
|
5587
|
-
name:
|
|
5642
|
+
name:
|
|
5643
|
+
msg.sender_name ||
|
|
5644
|
+
(msg.sender_type === 'customer' ? 'You' : 'Support'),
|
|
5588
5645
|
avatarUrl: msg.sender_avatar || null,
|
|
5589
5646
|
},
|
|
5590
5647
|
}));
|
|
@@ -6929,8 +6986,10 @@
|
|
|
6929
6986
|
// Check backend tracking first (from init response)
|
|
6930
6987
|
if (this.config.last_feedback_at) {
|
|
6931
6988
|
try {
|
|
6932
|
-
const backendTimestamp = new Date(
|
|
6933
|
-
|
|
6989
|
+
const backendTimestamp = new Date(
|
|
6990
|
+
this.config.last_feedback_at
|
|
6991
|
+
).getTime();
|
|
6992
|
+
if (now - backendTimestamp < cooldownMs) {
|
|
6934
6993
|
return true;
|
|
6935
6994
|
}
|
|
6936
6995
|
} catch (e) {
|
|
@@ -6945,7 +7004,7 @@
|
|
|
6945
7004
|
if (!stored) return false;
|
|
6946
7005
|
|
|
6947
7006
|
const data = JSON.parse(stored);
|
|
6948
|
-
return
|
|
7007
|
+
return now - data.submittedAt < cooldownMs;
|
|
6949
7008
|
} catch (e) {
|
|
6950
7009
|
return false;
|
|
6951
7010
|
}
|