@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.
@@ -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 === 'messagesUpdate' &&
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
- : 'Write a message...';
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
- <div class="messenger-message-bubble">
142
- <div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
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
- <div class="messenger-message-bubble">
156
- <div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
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">&times;</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
- input.addEventListener('input', () => {
270
- input.style.height = 'auto';
271
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
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
- sendBtn.disabled = !input.value.trim();
380
+ // Enable/disable send button
381
+ this._updateSendButtonState();
274
382
 
275
- if (input.value.trim()) {
276
- this._startTyping();
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
- input.addEventListener('keydown', (e) => {
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
- sendBtn.addEventListener('click', () => {
288
- this._sendMessage();
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
- _sendMessage() {
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
- if (this.options.onStartConversation) {
304
- this.options.onStartConversation(content);
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.element.querySelector('.messenger-compose-send').disabled = true;
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
- this.state.setActiveConversation(null);
205
- this.state.setView('chat');
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 (this.options.onStartNewConversation) {
208
- this.options.onStartNewConversation();
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
- <button class="messenger-home-message-btn">
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
- this.element
196
- .querySelector('.messenger-home-message-btn')
197
- .addEventListener('click', () => {
198
- this.state.setView('messages');
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')