@product7/feedback-sdk 1.3.0 → 1.3.2
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/feedback-sdk.js +2158 -663
- 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 +2 -1
- package/src/core/APIService.js +191 -5
- package/src/core/FeedbackSDK.js +3 -0
- package/src/styles/messengerStyles.js +580 -9
- package/src/widgets/MessengerWidget.js +247 -137
- package/src/widgets/messenger/MessengerState.js +31 -1
- package/src/widgets/messenger/views/ChatView.js +347 -29
- package/src/widgets/messenger/views/ConversationsView.js +20 -5
- package/src/widgets/messenger/views/HomeView.js +50 -10
- package/src/widgets/messenger/views/PreChatFormView.js +224 -0
|
@@ -84,8 +84,9 @@ export class MessengerState {
|
|
|
84
84
|
* Set active conversation for chat view
|
|
85
85
|
*/
|
|
86
86
|
setActiveConversation(conversationId) {
|
|
87
|
+
const previousConversationId = this.activeConversationId;
|
|
87
88
|
this.activeConversationId = conversationId;
|
|
88
|
-
this._notify('conversationChange', { conversationId });
|
|
89
|
+
this._notify('conversationChange', { conversationId, previousConversationId });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/**
|
|
@@ -137,6 +138,20 @@ export class MessengerState {
|
|
|
137
138
|
this._notify('messageAdded', { conversationId, message });
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Update a conversation by id
|
|
143
|
+
*/
|
|
144
|
+
updateConversation(conversationId, updates) {
|
|
145
|
+
const conv = this.conversations.find((c) => c.id === conversationId);
|
|
146
|
+
if (!conv) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Object.assign(conv, updates);
|
|
151
|
+
this._notify('conversationUpdated', { conversationId, conversation: conv });
|
|
152
|
+
return conv;
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
/**
|
|
141
156
|
* Mark conversation as read
|
|
142
157
|
*/
|
|
@@ -206,6 +221,21 @@ export class MessengerState {
|
|
|
206
221
|
return this.messages[this.activeConversationId] || [];
|
|
207
222
|
}
|
|
208
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Update team avatars from backend agent data.
|
|
226
|
+
* Converts available_agents ({full_name, picture}) into avatar strings
|
|
227
|
+
* the views already support (URL strings or initial strings).
|
|
228
|
+
*/
|
|
229
|
+
setTeamAvatarsFromAgents(agents) {
|
|
230
|
+
if (!agents || agents.length === 0) return;
|
|
231
|
+
|
|
232
|
+
this.teamAvatars = agents.map((agent) => {
|
|
233
|
+
if (agent.picture) return agent.picture;
|
|
234
|
+
return agent.full_name || '?';
|
|
235
|
+
});
|
|
236
|
+
this._notify('teamAvatarsUpdate', { teamAvatars: this.teamAvatars });
|
|
237
|
+
}
|
|
238
|
+
|
|
209
239
|
/**
|
|
210
240
|
* Get filtered help articles
|
|
211
241
|
*/
|
|
@@ -7,6 +7,9 @@ export class ChatView {
|
|
|
7
7
|
this._typingTimeout = null;
|
|
8
8
|
this._isTyping = false;
|
|
9
9
|
this._typingIndicator = null;
|
|
10
|
+
this._isConversationClosed = false;
|
|
11
|
+
this._showEmailOverlayFlag = false;
|
|
12
|
+
this._pendingAttachments = []; // { file, preview, type }
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
render() {
|
|
@@ -34,10 +37,12 @@ export class ChatView {
|
|
|
34
37
|
) {
|
|
35
38
|
this._hideTypingIndicator();
|
|
36
39
|
} else if (
|
|
37
|
-
type === '
|
|
40
|
+
type === 'conversationUpdated' &&
|
|
38
41
|
data.conversationId === this.state.activeConversationId
|
|
39
42
|
) {
|
|
40
43
|
this._updateContent();
|
|
44
|
+
} else if (type === 'messagesUpdate' && data.conversationId === this.state.activeConversationId) {
|
|
45
|
+
this._updateContent();
|
|
41
46
|
}
|
|
42
47
|
});
|
|
43
48
|
|
|
@@ -48,6 +53,8 @@ export class ChatView {
|
|
|
48
53
|
const conversation = this.state.getActiveConversation();
|
|
49
54
|
const messages = this.state.getActiveMessages();
|
|
50
55
|
const isNewConversation = !this.state.activeConversationId;
|
|
56
|
+
const isClosed = !isNewConversation && conversation?.status === 'closed';
|
|
57
|
+
this._isConversationClosed = isClosed;
|
|
51
58
|
|
|
52
59
|
const messagesHtml =
|
|
53
60
|
messages.length === 0
|
|
@@ -60,7 +67,11 @@ export class ChatView {
|
|
|
60
67
|
: conversation?.title || 'Chat with team';
|
|
61
68
|
const placeholder = isNewConversation
|
|
62
69
|
? 'Start typing your message...'
|
|
63
|
-
:
|
|
70
|
+
: isClosed
|
|
71
|
+
? 'Conversation closed'
|
|
72
|
+
: 'Write a message...';
|
|
73
|
+
|
|
74
|
+
const existingName = this.state.userContext?.name || '';
|
|
64
75
|
|
|
65
76
|
this.element.innerHTML = `
|
|
66
77
|
<div class="messenger-chat-header">
|
|
@@ -82,6 +93,12 @@ export class ChatView {
|
|
|
82
93
|
|
|
83
94
|
<div class="messenger-chat-messages">
|
|
84
95
|
${messagesHtml}
|
|
96
|
+
${isClosed ? `
|
|
97
|
+
<div class="messenger-closed-banner">
|
|
98
|
+
<i class="ph ph-check-circle" style="font-size: 18px;"></i>
|
|
99
|
+
<span>This conversation has been resolved</span>
|
|
100
|
+
</div>
|
|
101
|
+
` : ''}
|
|
85
102
|
<div class="messenger-typing-indicator" style="display: none;">
|
|
86
103
|
<div class="messenger-typing-dots">
|
|
87
104
|
<span></span><span></span><span></span>
|
|
@@ -90,7 +107,13 @@ export class ChatView {
|
|
|
90
107
|
</div>
|
|
91
108
|
</div>
|
|
92
109
|
|
|
110
|
+
${isClosed ? '' : `
|
|
111
|
+
<div class="messenger-compose-attachments-preview"></div>
|
|
112
|
+
|
|
93
113
|
<div class="messenger-chat-compose">
|
|
114
|
+
<button class="messenger-compose-attach" aria-label="Attach file">
|
|
115
|
+
<i class="ph ph-paperclip" style="font-size: 20px;"></i>
|
|
116
|
+
</button>
|
|
94
117
|
<div class="messenger-compose-input-wrapper">
|
|
95
118
|
<textarea class="messenger-compose-input" placeholder="${placeholder}" rows="1"></textarea>
|
|
96
119
|
</div>
|
|
@@ -99,6 +122,21 @@ export class ChatView {
|
|
|
99
122
|
<path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14L118.42,148.9l47.24-47.25a8,8,0,0,0-11.31-11.31L107.1,137.58,24,98.22l.14,0L216,40Z"></path>
|
|
100
123
|
</svg>
|
|
101
124
|
</button>
|
|
125
|
+
<input type="file" class="messenger-compose-file-input" style="display:none;" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip" />
|
|
126
|
+
</div>
|
|
127
|
+
`}
|
|
128
|
+
|
|
129
|
+
<div class="messenger-email-overlay" style="display: none;">
|
|
130
|
+
<div class="messenger-email-card">
|
|
131
|
+
<h4>What is your email address?</h4>
|
|
132
|
+
<p>Enter your email to know when we reply:</p>
|
|
133
|
+
<input type="text" class="messenger-email-name" placeholder="Name (optional)" value="${this._escapeHtml(existingName)}" autocomplete="name" />
|
|
134
|
+
<input type="email" class="messenger-email-input" placeholder="Enter your email address..." autocomplete="email" />
|
|
135
|
+
<div class="messenger-email-actions">
|
|
136
|
+
<button class="messenger-email-submit" disabled>Set my email</button>
|
|
137
|
+
<button class="messenger-email-skip">Skip</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
102
140
|
</div>
|
|
103
141
|
`;
|
|
104
142
|
|
|
@@ -107,6 +145,12 @@ export class ChatView {
|
|
|
107
145
|
);
|
|
108
146
|
this._attachEvents();
|
|
109
147
|
this._scrollToBottom();
|
|
148
|
+
this._renderAttachmentPreviews();
|
|
149
|
+
|
|
150
|
+
// Show email overlay after first message sent without email
|
|
151
|
+
if (this._showEmailOverlayFlag) {
|
|
152
|
+
this._showEmailOverlay();
|
|
153
|
+
}
|
|
110
154
|
}
|
|
111
155
|
|
|
112
156
|
_renderEmptyState(isNewConversation = false) {
|
|
@@ -128,19 +172,37 @@ export class ChatView {
|
|
|
128
172
|
`;
|
|
129
173
|
}
|
|
130
174
|
|
|
175
|
+
_renderMessageAttachments(attachments) {
|
|
176
|
+
if (!attachments || attachments.length === 0) return '';
|
|
177
|
+
return attachments.map((att) => {
|
|
178
|
+
if (att.type === 'image') {
|
|
179
|
+
return `<img class="messenger-message-image" src="${this._escapeHtml(att.url)}" alt="${this._escapeHtml(att.name || 'image')}" data-url="${this._escapeHtml(att.url)}" />`;
|
|
180
|
+
}
|
|
181
|
+
return `<a class="messenger-message-file" href="${this._escapeHtml(att.url)}" data-url="${this._escapeHtml(att.url)}" data-name="${this._escapeHtml(att.name || 'file')}">
|
|
182
|
+
<i class="ph ph-file" style="font-size:16px;"></i>
|
|
183
|
+
<span>${this._escapeHtml(att.name || 'file')}</span>
|
|
184
|
+
<i class="ph ph-download-simple messenger-file-download-icon" style="font-size:14px;"></i>
|
|
185
|
+
</a>`;
|
|
186
|
+
}).join('');
|
|
187
|
+
}
|
|
188
|
+
|
|
131
189
|
_renderMessage(message) {
|
|
132
190
|
const isOwn = message.isOwn;
|
|
133
191
|
const messageClass = isOwn
|
|
134
192
|
? 'messenger-message-own'
|
|
135
193
|
: 'messenger-message-received';
|
|
136
194
|
const timeStr = this._formatMessageTime(message.timestamp);
|
|
195
|
+
const attachmentsHtml = this._renderMessageAttachments(message.attachments);
|
|
196
|
+
|
|
197
|
+
const contentHtml = message.content ? `<div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>` : '';
|
|
198
|
+
|
|
199
|
+
const bubbleHtml = contentHtml ? `<div class="messenger-message-bubble">${contentHtml}</div>` : '';
|
|
137
200
|
|
|
138
201
|
if (isOwn) {
|
|
139
202
|
return `
|
|
140
203
|
<div class="messenger-message ${messageClass}">
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</div>
|
|
204
|
+
${bubbleHtml}
|
|
205
|
+
${attachmentsHtml}
|
|
144
206
|
<div class="messenger-message-time">${timeStr}</div>
|
|
145
207
|
</div>
|
|
146
208
|
`;
|
|
@@ -152,9 +214,8 @@ export class ChatView {
|
|
|
152
214
|
<div class="messenger-message-avatar">${avatarHtml}</div>
|
|
153
215
|
<div class="messenger-message-wrapper">
|
|
154
216
|
<div class="messenger-message-sender">${message.sender?.name || 'Support'}</div>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
</div>
|
|
217
|
+
${bubbleHtml}
|
|
218
|
+
${attachmentsHtml}
|
|
158
219
|
<div class="messenger-message-time">${timeStr}</div>
|
|
159
220
|
</div>
|
|
160
221
|
</div>
|
|
@@ -250,6 +311,49 @@ export class ChatView {
|
|
|
250
311
|
}
|
|
251
312
|
}
|
|
252
313
|
|
|
314
|
+
_updateSendButtonState() {
|
|
315
|
+
const input = this.element.querySelector('.messenger-compose-input');
|
|
316
|
+
const sendBtn = this.element.querySelector('.messenger-compose-send');
|
|
317
|
+
if (input && sendBtn) {
|
|
318
|
+
sendBtn.disabled = !input.value.trim() && this._pendingAttachments.length === 0;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_renderAttachmentPreviews() {
|
|
323
|
+
const container = this.element.querySelector('.messenger-compose-attachments-preview');
|
|
324
|
+
if (!container) return;
|
|
325
|
+
|
|
326
|
+
if (this._pendingAttachments.length === 0) {
|
|
327
|
+
container.innerHTML = '';
|
|
328
|
+
container.style.display = 'none';
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
container.style.display = 'flex';
|
|
333
|
+
container.innerHTML = this._pendingAttachments.map((att, i) => {
|
|
334
|
+
const isImage = att.type.startsWith('image');
|
|
335
|
+
const thumb = isImage
|
|
336
|
+
? `<img class="messenger-attachment-thumb" src="${att.preview}" alt="${this._escapeHtml(att.file.name)}" />`
|
|
337
|
+
: `<div class="messenger-attachment-thumb messenger-attachment-file-icon"><i class="ph ph-file" style="font-size:20px;"></i></div>`;
|
|
338
|
+
return `
|
|
339
|
+
<div class="messenger-attachment-preview" data-index="${i}">
|
|
340
|
+
${thumb}
|
|
341
|
+
<button class="messenger-attachment-remove" data-index="${i}" aria-label="Remove">×</button>
|
|
342
|
+
</div>
|
|
343
|
+
`;
|
|
344
|
+
}).join('');
|
|
345
|
+
|
|
346
|
+
// Attach remove button events
|
|
347
|
+
container.querySelectorAll('.messenger-attachment-remove').forEach((btn) => {
|
|
348
|
+
btn.addEventListener('click', (e) => {
|
|
349
|
+
const idx = parseInt(e.currentTarget.dataset.index, 10);
|
|
350
|
+
this._pendingAttachments.splice(idx, 1);
|
|
351
|
+
this._renderAttachmentPreviews();
|
|
352
|
+
this._updateSendButtonState();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
253
357
|
_attachEvents() {
|
|
254
358
|
this.element
|
|
255
359
|
.querySelector('.messenger-back-btn')
|
|
@@ -263,45 +367,250 @@ export class ChatView {
|
|
|
263
367
|
this.state.setOpen(false);
|
|
264
368
|
});
|
|
265
369
|
|
|
370
|
+
// Compose input (not rendered when conversation is closed)
|
|
266
371
|
const input = this.element.querySelector('.messenger-compose-input');
|
|
267
372
|
const sendBtn = this.element.querySelector('.messenger-compose-send');
|
|
268
373
|
|
|
269
|
-
|
|
270
|
-
input.
|
|
271
|
-
|
|
374
|
+
if (input && sendBtn) {
|
|
375
|
+
input.addEventListener('input', () => {
|
|
376
|
+
// Auto-resize textarea
|
|
377
|
+
input.style.height = 'auto';
|
|
378
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
272
379
|
|
|
273
|
-
|
|
380
|
+
// Enable/disable send button
|
|
381
|
+
this._updateSendButtonState();
|
|
274
382
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
383
|
+
// Send typing indicator
|
|
384
|
+
if (input.value.trim()) {
|
|
385
|
+
this._startTyping();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
input.addEventListener('keydown', (e) => {
|
|
390
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
this._sendMessage();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
279
395
|
|
|
280
|
-
|
|
281
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
282
|
-
e.preventDefault();
|
|
396
|
+
sendBtn.addEventListener('click', () => {
|
|
283
397
|
this._sendMessage();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Attach button + file input
|
|
402
|
+
const attachBtn = this.element.querySelector('.messenger-compose-attach');
|
|
403
|
+
const fileInput = this.element.querySelector('.messenger-compose-file-input');
|
|
404
|
+
|
|
405
|
+
if (attachBtn && fileInput) {
|
|
406
|
+
attachBtn.addEventListener('click', () => {
|
|
407
|
+
fileInput.click();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
fileInput.addEventListener('change', (e) => {
|
|
411
|
+
const files = e.target.files;
|
|
412
|
+
if (!files) return;
|
|
413
|
+
Array.from(files).forEach((file) => {
|
|
414
|
+
const reader = new FileReader();
|
|
415
|
+
reader.onload = (ev) => {
|
|
416
|
+
this._pendingAttachments.push({
|
|
417
|
+
file,
|
|
418
|
+
preview: ev.target.result,
|
|
419
|
+
type: file.type,
|
|
420
|
+
});
|
|
421
|
+
this._renderAttachmentPreviews();
|
|
422
|
+
this._updateSendButtonState();
|
|
423
|
+
};
|
|
424
|
+
reader.readAsDataURL(file);
|
|
425
|
+
});
|
|
426
|
+
fileInput.value = '';
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Email overlay events
|
|
431
|
+
const emailInput = this.element.querySelector('.messenger-email-input');
|
|
432
|
+
const emailSubmit = this.element.querySelector('.messenger-email-submit');
|
|
433
|
+
const emailSkip = this.element.querySelector('.messenger-email-skip');
|
|
434
|
+
|
|
435
|
+
if (emailInput) {
|
|
436
|
+
emailInput.addEventListener('input', () => {
|
|
437
|
+
const isValid = this._isValidEmail(emailInput.value.trim());
|
|
438
|
+
emailSubmit.disabled = !isValid;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
emailInput.addEventListener('keydown', (e) => {
|
|
442
|
+
if (e.key === 'Enter' && !emailSubmit.disabled) {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
this._handleEmailSubmit();
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (emailSubmit) {
|
|
450
|
+
emailSubmit.addEventListener('click', () => {
|
|
451
|
+
this._handleEmailSubmit();
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (emailSkip) {
|
|
456
|
+
emailSkip.addEventListener('click', () => {
|
|
457
|
+
this._hideEmailOverlay();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Delegated events for attachment clicks
|
|
462
|
+
const messagesContainer = this.element.querySelector('.messenger-chat-messages');
|
|
463
|
+
if (messagesContainer) {
|
|
464
|
+
messagesContainer.addEventListener('click', (e) => {
|
|
465
|
+
// File click -> download
|
|
466
|
+
const fileLink = e.target.closest('.messenger-message-file');
|
|
467
|
+
if (fileLink) {
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
const url = fileLink.dataset.url;
|
|
470
|
+
const name = fileLink.dataset.name;
|
|
471
|
+
this._downloadFile(url, name);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Image click -> open in new tab
|
|
476
|
+
const img = e.target.closest('.messenger-message-image');
|
|
477
|
+
if (img) {
|
|
478
|
+
const url = img.dataset.url || img.src;
|
|
479
|
+
window.open(url, '_blank');
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async _downloadFile(url, name) {
|
|
486
|
+
try {
|
|
487
|
+
const response = await fetch(url);
|
|
488
|
+
const blob = await response.blob();
|
|
489
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
490
|
+
const a = document.createElement('a');
|
|
491
|
+
a.href = blobUrl;
|
|
492
|
+
a.download = name || 'download';
|
|
493
|
+
a.style.display = 'none';
|
|
494
|
+
document.body.appendChild(a);
|
|
495
|
+
a.click();
|
|
496
|
+
document.body.removeChild(a);
|
|
497
|
+
URL.revokeObjectURL(blobUrl);
|
|
498
|
+
} catch {
|
|
499
|
+
// Fallback: open in new tab
|
|
500
|
+
window.open(url, '_blank');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_escapeHtml(text) {
|
|
505
|
+
if (!text) return '';
|
|
506
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_isValidEmail(email) {
|
|
510
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
_showEmailOverlay() {
|
|
514
|
+
const overlay = this.element.querySelector('.messenger-email-overlay');
|
|
515
|
+
if (overlay) {
|
|
516
|
+
overlay.style.display = 'flex';
|
|
517
|
+
const emailInput = overlay.querySelector('.messenger-email-input');
|
|
518
|
+
if (emailInput) {
|
|
519
|
+
setTimeout(() => emailInput.focus(), 100);
|
|
284
520
|
}
|
|
285
|
-
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
286
523
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
524
|
+
_startPendingConversation() {
|
|
525
|
+
if (this._pendingMessage && this.options.onStartConversation) {
|
|
526
|
+
this.options.onStartConversation(this._pendingMessage, this._pendingAttachmentsForSend || []);
|
|
527
|
+
this._pendingMessage = null;
|
|
528
|
+
this._pendingAttachmentsForSend = null;
|
|
529
|
+
}
|
|
290
530
|
}
|
|
291
531
|
|
|
292
|
-
|
|
532
|
+
_hideEmailOverlay() {
|
|
533
|
+
this._showEmailOverlayFlag = false;
|
|
534
|
+
const overlay = this.element.querySelector('.messenger-email-overlay');
|
|
535
|
+
if (overlay) {
|
|
536
|
+
overlay.style.display = 'none';
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async _handleEmailSubmit() {
|
|
541
|
+
const nameInput = this.element.querySelector('.messenger-email-name');
|
|
542
|
+
const emailInput = this.element.querySelector('.messenger-email-input');
|
|
543
|
+
const submitBtn = this.element.querySelector('.messenger-email-submit');
|
|
544
|
+
|
|
545
|
+
const name = nameInput?.value.trim() || '';
|
|
546
|
+
const email = emailInput?.value.trim();
|
|
547
|
+
|
|
548
|
+
if (!email || !this._isValidEmail(email)) return;
|
|
549
|
+
|
|
550
|
+
submitBtn.disabled = true;
|
|
551
|
+
submitBtn.textContent = 'Saving...';
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
if (this.options.onIdentifyContact) {
|
|
555
|
+
await this.options.onIdentifyContact({ name, email });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!this.state.userContext) {
|
|
559
|
+
this.state.userContext = {};
|
|
560
|
+
}
|
|
561
|
+
this.state.userContext.name = name;
|
|
562
|
+
this.state.userContext.email = email;
|
|
563
|
+
|
|
564
|
+
this._hideEmailOverlay();
|
|
565
|
+
this._startPendingConversation();
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error('[ChatView] Failed to save email:', error);
|
|
568
|
+
submitBtn.disabled = false;
|
|
569
|
+
submitBtn.textContent = 'Set my email';
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async _sendMessage() {
|
|
574
|
+
if (this._isConversationClosed) return;
|
|
293
575
|
const input = this.element.querySelector('.messenger-compose-input');
|
|
294
576
|
const content = input.value.trim();
|
|
577
|
+
const hasAttachments = this._pendingAttachments.length > 0;
|
|
295
578
|
|
|
296
|
-
if (!content) return;
|
|
579
|
+
if (!content && !hasAttachments) return;
|
|
297
580
|
|
|
298
581
|
this._stopTyping();
|
|
299
582
|
|
|
583
|
+
// Collect attachments to upload
|
|
584
|
+
const attachmentsToSend = [...this._pendingAttachments];
|
|
585
|
+
|
|
300
586
|
const isNewConversation = !this.state.activeConversationId;
|
|
587
|
+
const needsContactInfo = !this.state.userContext?.email;
|
|
301
588
|
|
|
302
589
|
if (isNewConversation) {
|
|
303
|
-
|
|
304
|
-
|
|
590
|
+
// Show user's message in chat immediately
|
|
591
|
+
const localMessage = {
|
|
592
|
+
id: 'msg_' + Date.now(),
|
|
593
|
+
content: content,
|
|
594
|
+
isOwn: true,
|
|
595
|
+
timestamp: new Date().toISOString(),
|
|
596
|
+
attachments: attachmentsToSend.map((a) => ({
|
|
597
|
+
url: a.preview,
|
|
598
|
+
type: a.type.startsWith('image') ? 'image' : 'file',
|
|
599
|
+
name: a.file.name,
|
|
600
|
+
})),
|
|
601
|
+
};
|
|
602
|
+
this._appendMessage(localMessage);
|
|
603
|
+
this._scrollToBottom();
|
|
604
|
+
|
|
605
|
+
if (needsContactInfo) {
|
|
606
|
+
this._pendingMessage = content;
|
|
607
|
+
this._pendingAttachmentsForSend = attachmentsToSend;
|
|
608
|
+
this._showEmailOverlayFlag = true;
|
|
609
|
+
setTimeout(() => this._showEmailOverlay(), 300);
|
|
610
|
+
} else {
|
|
611
|
+
if (this.options.onStartConversation) {
|
|
612
|
+
this.options.onStartConversation(content, attachmentsToSend);
|
|
613
|
+
}
|
|
305
614
|
}
|
|
306
615
|
} else {
|
|
307
616
|
const message = {
|
|
@@ -309,21 +618,30 @@ export class ChatView {
|
|
|
309
618
|
content: content,
|
|
310
619
|
isOwn: true,
|
|
311
620
|
timestamp: new Date().toISOString(),
|
|
621
|
+
attachments: attachmentsToSend.map((a) => ({
|
|
622
|
+
url: a.preview,
|
|
623
|
+
type: a.type.startsWith('image') ? 'image' : 'file',
|
|
624
|
+
name: a.file.name,
|
|
625
|
+
})),
|
|
312
626
|
};
|
|
313
627
|
|
|
314
628
|
this.state.addMessage(this.state.activeConversationId, message);
|
|
315
629
|
|
|
316
630
|
if (this.options.onSendMessage) {
|
|
317
|
-
this.options.onSendMessage(this.state.activeConversationId, message);
|
|
631
|
+
this.options.onSendMessage(this.state.activeConversationId, message, attachmentsToSend);
|
|
318
632
|
}
|
|
319
633
|
}
|
|
320
634
|
|
|
635
|
+
// Clear input and attachments
|
|
321
636
|
input.value = '';
|
|
322
637
|
input.style.height = 'auto';
|
|
323
|
-
this.
|
|
638
|
+
this._pendingAttachments = [];
|
|
639
|
+
this._renderAttachmentPreviews();
|
|
640
|
+
this._updateSendButtonState();
|
|
324
641
|
}
|
|
325
642
|
|
|
326
643
|
_startTyping() {
|
|
644
|
+
if (this._isConversationClosed) return;
|
|
327
645
|
if (!this._isTyping && this.state.activeConversationId) {
|
|
328
646
|
this._isTyping = true;
|
|
329
647
|
if (this.options.onTyping) {
|
|
@@ -17,7 +17,8 @@ export class ConversationsView {
|
|
|
17
17
|
if (
|
|
18
18
|
type === 'conversationsUpdate' ||
|
|
19
19
|
type === 'conversationAdded' ||
|
|
20
|
-
type === 'conversationRead'
|
|
20
|
+
type === 'conversationRead' ||
|
|
21
|
+
type === 'conversationUpdated'
|
|
21
22
|
) {
|
|
22
23
|
this._updateContent();
|
|
23
24
|
}
|
|
@@ -201,11 +202,25 @@ export class ConversationsView {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
_startNewConversation() {
|
|
204
|
-
|
|
205
|
-
this.state.
|
|
205
|
+
// If there's an open conversation, route to it instead of creating new
|
|
206
|
+
const openConversation = this.state.conversations.find(
|
|
207
|
+
(c) => c.status === 'open'
|
|
208
|
+
);
|
|
206
209
|
|
|
207
|
-
if (
|
|
208
|
-
this.
|
|
210
|
+
if (openConversation) {
|
|
211
|
+
this.state.setActiveConversation(openConversation.id);
|
|
212
|
+
this.state.markAsRead(openConversation.id);
|
|
213
|
+
this.state.setView('chat');
|
|
214
|
+
if (this.options.onSelectConversation) {
|
|
215
|
+
this.options.onSelectConversation(openConversation.id);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
this.state.setActiveConversation(null);
|
|
219
|
+
if (this.options.onStartNewConversation) {
|
|
220
|
+
this.options.onStartNewConversation();
|
|
221
|
+
} else {
|
|
222
|
+
this.state.setView('chat');
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
}
|
|
211
226
|
|
|
@@ -50,12 +50,7 @@ export class HomeView {
|
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
<div class="messenger-home-body">
|
|
53
|
-
|
|
54
|
-
<span>Send us a message</span>
|
|
55
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256">
|
|
56
|
-
<path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path>
|
|
57
|
-
</svg>
|
|
58
|
-
</button>
|
|
53
|
+
${this._renderMessageButton()}
|
|
59
54
|
|
|
60
55
|
${this._renderFeaturedCard()}
|
|
61
56
|
|
|
@@ -117,6 +112,37 @@ export class HomeView {
|
|
|
117
112
|
`;
|
|
118
113
|
}
|
|
119
114
|
|
|
115
|
+
_renderMessageButton() {
|
|
116
|
+
const openConversation = this.state.conversations.find(
|
|
117
|
+
(c) => c.status === 'open'
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (openConversation) {
|
|
121
|
+
const preview = openConversation.lastMessage
|
|
122
|
+
? (openConversation.lastMessage.length > 40
|
|
123
|
+
? openConversation.lastMessage.substring(0, 40) + '...'
|
|
124
|
+
: openConversation.lastMessage)
|
|
125
|
+
: 'Continue your conversation';
|
|
126
|
+
|
|
127
|
+
return `
|
|
128
|
+
<button class="messenger-home-message-btn messenger-home-continue-btn" data-conversation-id="${openConversation.id}">
|
|
129
|
+
<div class="messenger-home-continue-info">
|
|
130
|
+
<span class="messenger-home-continue-label">Continue conversation</span>
|
|
131
|
+
<span class="messenger-home-continue-preview">${preview}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<i class="ph ph-arrow-right" style="font-size: 16px;"></i>
|
|
134
|
+
</button>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `
|
|
139
|
+
<button class="messenger-home-message-btn">
|
|
140
|
+
<span>Send us a message</span>
|
|
141
|
+
<i class="ph ph-arrow-right" style="font-size: 16px;"></i>
|
|
142
|
+
</button>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
_renderFeaturedCard() {
|
|
121
147
|
if (!this.options.featuredContent) {
|
|
122
148
|
return '';
|
|
@@ -192,11 +218,25 @@ export class HomeView {
|
|
|
192
218
|
this.state.setOpen(false);
|
|
193
219
|
});
|
|
194
220
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
221
|
+
// Send message / continue conversation button
|
|
222
|
+
const msgBtn = this.element.querySelector('.messenger-home-message-btn');
|
|
223
|
+
if (msgBtn) {
|
|
224
|
+
msgBtn.addEventListener('click', () => {
|
|
225
|
+
const convId = msgBtn.dataset.conversationId;
|
|
226
|
+
if (convId) {
|
|
227
|
+
// Continue existing open conversation
|
|
228
|
+
this.state.setActiveConversation(convId);
|
|
229
|
+
this.state.setView('chat');
|
|
230
|
+
if (this.options.onSelectConversation) {
|
|
231
|
+
this.options.onSelectConversation(convId);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// No open conversation — start new
|
|
235
|
+
this.state.setActiveConversation(null);
|
|
236
|
+
this.state.setView('chat');
|
|
237
|
+
}
|
|
199
238
|
});
|
|
239
|
+
}
|
|
200
240
|
|
|
201
241
|
this.element
|
|
202
242
|
.querySelectorAll('.messenger-home-changelog-item')
|