@product7/product7-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1025 -0
  2. package/dist/README.md +1025 -0
  3. package/dist/product7-js.js +14658 -0
  4. package/dist/product7-js.js.map +1 -0
  5. package/dist/product7-js.min.js +2 -0
  6. package/dist/product7-js.min.js.map +1 -0
  7. package/package.json +114 -0
  8. package/src/api/mock-data/index.js +360 -0
  9. package/src/api/services/ChangelogService.js +28 -0
  10. package/src/api/services/FeedbackService.js +44 -0
  11. package/src/api/services/HelpService.js +50 -0
  12. package/src/api/services/MessengerService.js +279 -0
  13. package/src/api/services/SurveyService.js +127 -0
  14. package/src/api/utils/helpers.js +30 -0
  15. package/src/core/APIService.js +303 -0
  16. package/src/core/BaseAPIService.js +298 -0
  17. package/src/core/EventBus.js +54 -0
  18. package/src/core/Product7.js +812 -0
  19. package/src/core/WebSocketService.js +275 -0
  20. package/src/docs/api.md +226 -0
  21. package/src/docs/example.md +461 -0
  22. package/src/docs/framework-integrations.md +714 -0
  23. package/src/docs/installation.md +281 -0
  24. package/src/index.js +894 -0
  25. package/src/styles/base.js +50 -0
  26. package/src/styles/changelog.js +665 -0
  27. package/src/styles/components.js +553 -0
  28. package/src/styles/design-tokens.js +124 -0
  29. package/src/styles/feedback.js +325 -0
  30. package/src/styles/messenger-components.js +632 -0
  31. package/src/styles/messenger-core.js +233 -0
  32. package/src/styles/messenger-features.js +169 -0
  33. package/src/styles/messenger-views.js +877 -0
  34. package/src/styles/messenger.js +17 -0
  35. package/src/styles/messengerCustomStyles.js +114 -0
  36. package/src/styles/styles.js +26 -0
  37. package/src/styles/survey.js +894 -0
  38. package/src/utils/errors.js +142 -0
  39. package/src/utils/helpers.js +219 -0
  40. package/src/widgets/BaseWidget.js +548 -0
  41. package/src/widgets/ButtonWidget.js +104 -0
  42. package/src/widgets/ChangelogWidget.js +615 -0
  43. package/src/widgets/InlineWidget.js +148 -0
  44. package/src/widgets/MessengerWidget.js +979 -0
  45. package/src/widgets/SurveyWidget.js +1325 -0
  46. package/src/widgets/TabWidget.js +45 -0
  47. package/src/widgets/WidgetFactory.js +70 -0
  48. package/src/widgets/messenger/MessengerState.js +323 -0
  49. package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
  50. package/src/widgets/messenger/components/MessengerPanel.js +111 -0
  51. package/src/widgets/messenger/components/NavigationTabs.js +130 -0
  52. package/src/widgets/messenger/views/ChangelogView.js +167 -0
  53. package/src/widgets/messenger/views/ChatView.js +592 -0
  54. package/src/widgets/messenger/views/ConversationsView.js +244 -0
  55. package/src/widgets/messenger/views/HelpView.js +239 -0
  56. package/src/widgets/messenger/views/HomeView.js +300 -0
  57. package/src/widgets/messenger/views/PreChatFormView.js +109 -0
  58. package/types/index.d.ts +341 -0
@@ -0,0 +1,592 @@
1
+ export class ChatView {
2
+ constructor(state, options = {}) {
3
+ this.state = state;
4
+ this.options = options;
5
+ this.element = null;
6
+ this._unsubscribe = null;
7
+ this._typingTimeout = null;
8
+ this._isTyping = false;
9
+ this._typingIndicator = null;
10
+ this._isConversationClosed = false;
11
+ this._pendingAttachments = [];
12
+ }
13
+
14
+ render() {
15
+ this.element = document.createElement('div');
16
+ this.element.className = 'messenger-view messenger-chat-view';
17
+
18
+ this._updateContent();
19
+
20
+ this._unsubscribe = this.state.subscribe((type, data) => {
21
+ if (
22
+ type === 'messageAdded' &&
23
+ data.conversationId === this.state.activeConversationId
24
+ ) {
25
+ this._hideTypingIndicator();
26
+ this._appendMessage(data.message);
27
+ this._scrollToBottom();
28
+ } else if (
29
+ type === 'typingStarted' &&
30
+ data.conversationId === this.state.activeConversationId
31
+ ) {
32
+ this._showTypingIndicator(data.userName);
33
+ } else if (
34
+ type === 'typingStopped' &&
35
+ data.conversationId === this.state.activeConversationId
36
+ ) {
37
+ this._hideTypingIndicator();
38
+ } else if (
39
+ type === 'conversationUpdated' &&
40
+ data.conversationId === this.state.activeConversationId
41
+ ) {
42
+ this._updateContent();
43
+ } else if (
44
+ type === 'messagesUpdate' &&
45
+ data.conversationId === this.state.activeConversationId
46
+ ) {
47
+ this._updateContent();
48
+ }
49
+ });
50
+
51
+ return this.element;
52
+ }
53
+
54
+ _updateContent() {
55
+ const conversation = this.state.getActiveConversation();
56
+ const messages = this.state.getActiveMessages();
57
+ const isNewConversation = !this.state.activeConversationId;
58
+ const isClosed = !isNewConversation && conversation?.status === 'closed';
59
+ this._isConversationClosed = isClosed;
60
+
61
+ const messagesHtml =
62
+ messages.length === 0
63
+ ? this._renderEmptyState(isNewConversation)
64
+ : messages.map((msg) => this._renderMessage(msg)).join('');
65
+
66
+ const title = isNewConversation
67
+ ? 'New conversation'
68
+ : conversation?.title || 'Chat with team';
69
+ const placeholder = isNewConversation
70
+ ? 'Start typing your message...'
71
+ : isClosed
72
+ ? 'Conversation closed'
73
+ : 'Write a message...';
74
+
75
+ this.element.innerHTML = `
76
+ <div class="messenger-chat-header">
77
+ <button class="sdk-btn-icon messenger-back-btn" aria-label="Back">
78
+ <iconify-icon icon="ph:arrow-left" width="20" height="20"></iconify-icon>
79
+ </button>
80
+ <div class="messenger-chat-header-avatar">
81
+ <iconify-icon icon="ph:lightbulb-duotone" width="20" height="20"></iconify-icon>
82
+ </div>
83
+ <div class="messenger-chat-header-info">
84
+ <span class="messenger-chat-title">${title}</span>
85
+ <span class="messenger-chat-subtitle">The team can also help</span>
86
+ </div>
87
+ <div class="messenger-chat-header-actions">
88
+ <button class="sdk-btn-icon" aria-label="More options">
89
+ <iconify-icon icon="ph:dots-three-outline-duotone" width="20" height="20"></iconify-icon>
90
+ </button>
91
+ <button class="sdk-close-btn" aria-label="Close">
92
+ <iconify-icon icon="ph:x-duotone" width="18" height="18"></iconify-icon>
93
+ </button>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="messenger-chat-messages">
98
+ ${messagesHtml}
99
+ ${
100
+ isClosed
101
+ ? `
102
+ <div class="messenger-closed-banner">
103
+ <iconify-icon icon="ph:check-circle-duotone" width="18" height="18"></iconify-icon>
104
+ <span>This conversation has been resolved</span>
105
+ </div>
106
+ `
107
+ : ''
108
+ }
109
+ <div class="messenger-typing-indicator">
110
+ <div class="messenger-typing-dots">
111
+ <span></span><span></span><span></span>
112
+ </div>
113
+ <span class="messenger-typing-text"></span>
114
+ </div>
115
+ </div>
116
+
117
+ ${
118
+ isClosed
119
+ ? ''
120
+ : `
121
+ <div class="messenger-compose-attachments-preview"></div>
122
+
123
+ <div class="messenger-chat-compose">
124
+ <div class="messenger-compose-input-wrapper">
125
+ <textarea class="messenger-compose-input" placeholder="${placeholder}" rows="1"></textarea>
126
+ </div>
127
+ <div class="messenger-compose-bottom">
128
+ <div class="messenger-compose-actions">
129
+ <button class="sdk-btn-icon messenger-compose-attach" aria-label="Attach file">
130
+ <iconify-icon icon="ph:paperclip-duotone" width="20" height="20"></iconify-icon>
131
+ </button>
132
+ <button class="sdk-btn-icon" aria-label="Emoji">
133
+ <iconify-icon icon="ph:smiley-duotone" width="20" height="20"></iconify-icon>
134
+ </button>
135
+ </div>
136
+ <button class="messenger-compose-send" aria-label="Send" disabled>
137
+ <iconify-icon icon="ph:paper-plane-right" width="20" height="20"></iconify-icon>
138
+ </button>
139
+ </div>
140
+ <input type="file" class="messenger-compose-file-input" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip" />
141
+ </div>
142
+ `
143
+ }
144
+ `;
145
+
146
+ this._typingIndicator = this.element.querySelector(
147
+ '.messenger-typing-indicator'
148
+ );
149
+ this._attachEvents();
150
+ this._scrollToBottom();
151
+ this._renderAttachmentPreviews();
152
+ }
153
+
154
+ _renderEmptyState(isNewConversation = false) {
155
+ const avatarHtml = this._renderTeamAvatars();
156
+ const responseTime =
157
+ this.state.responseTime || 'We typically reply within a few minutes';
158
+ const isOnline = this.state.agentsOnline;
159
+
160
+ return `
161
+ <div class="messenger-chat-empty">
162
+ <div class="messenger-chat-empty-avatars">${avatarHtml}</div>
163
+ <h3>${isNewConversation ? 'Start a new conversation' : 'Start the conversation'}</h3>
164
+ <p>Send us a message and we'll get back to you as soon as possible.</p>
165
+ <div class="messenger-chat-availability">
166
+ <span class="messenger-availability-dot ${isOnline ? 'messenger-availability-online' : 'messenger-availability-away'}"></span>
167
+ <span>${isOnline ? "We're online now" : responseTime}</span>
168
+ </div>
169
+ </div>
170
+ `;
171
+ }
172
+
173
+ _renderMessageAttachments(attachments) {
174
+ if (!attachments || attachments.length === 0) return '';
175
+ return attachments
176
+ .map((att) => {
177
+ if (att.type === 'image') {
178
+ return `<img class="messenger-message-image" src="${this._escapeHtml(att.url)}" alt="${this._escapeHtml(att.name || 'image')}" data-url="${this._escapeHtml(att.url)}" />`;
179
+ }
180
+ return `<a class="messenger-message-file" href="${this._escapeHtml(att.url)}" data-url="${this._escapeHtml(att.url)}" data-name="${this._escapeHtml(att.name || 'file')}">
181
+ <iconify-icon icon="ph:file-duotone" width="18" height="18"></iconify-icon>
182
+ <span>${this._escapeHtml(att.name || 'file')}</span>
183
+ <iconify-icon icon="ph:download-simple" width="16" height="16" class="messenger-file-download-icon"></iconify-icon>
184
+ </a>`;
185
+ })
186
+ .join('');
187
+ }
188
+
189
+ _renderMessage(message) {
190
+ const isOwn = message.isOwn;
191
+ const messageClass = isOwn
192
+ ? 'messenger-message-own'
193
+ : 'messenger-message-received';
194
+ const timeStr = this._formatMessageTime(message.timestamp);
195
+ const attachmentsHtml = this._renderMessageAttachments(message.attachments);
196
+
197
+ const contentHtml = message.content
198
+ ? `<div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>`
199
+ : '';
200
+ const bubbleHtml = contentHtml
201
+ ? `<div class="messenger-message-bubble">${contentHtml}</div>`
202
+ : '';
203
+
204
+ if (isOwn) {
205
+ return `
206
+ <div class="messenger-message ${messageClass}">
207
+ ${bubbleHtml}
208
+ ${attachmentsHtml}
209
+ </div>
210
+ `;
211
+ }
212
+
213
+ const senderName = message.sender?.name || 'Support';
214
+ const senderRole = message.sender?.role || 'Agent';
215
+ const avatarHtml = this._renderSenderAvatar(message.sender);
216
+ return `
217
+ <div class="messenger-message ${messageClass}">
218
+ <div class="messenger-message-row">
219
+ <div class="messenger-message-avatar">${avatarHtml}</div>
220
+ <div class="messenger-message-wrapper">
221
+ ${bubbleHtml}
222
+ ${attachmentsHtml}
223
+ </div>
224
+ </div>
225
+ <div class="messenger-message-meta">
226
+ <span>${senderName}</span>
227
+ <span>·</span>
228
+ <span>${senderRole}</span>
229
+ <span>·</span>
230
+ <span>${timeStr}</span>
231
+ </div>
232
+ </div>
233
+ `;
234
+ }
235
+
236
+ _renderSenderAvatar(sender) {
237
+ if (sender?.avatarUrl) {
238
+ return `<div class="sdk-avatar sdk-avatar-sm"><img src="${sender.avatarUrl}" alt="${sender.name}" /></div>`;
239
+ }
240
+ const initial = (sender?.name || 'S').charAt(0).toUpperCase();
241
+ return `<div class="sdk-avatar sdk-avatar-sm">${initial}</div>`;
242
+ }
243
+
244
+ _renderTeamAvatars() {
245
+ const avatars = this.state.teamAvatars;
246
+ if (!avatars || avatars.length === 0) {
247
+ return `
248
+ <div class="messenger-avatar-stack">
249
+ <div class="sdk-avatar sdk-avatar-md">S</div>
250
+ <div class="sdk-avatar sdk-avatar-md">T</div>
251
+ </div>
252
+ `;
253
+ }
254
+
255
+ const avatarItems = avatars
256
+ .slice(0, 3)
257
+ .map((avatar) => {
258
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
259
+ return `<div class="sdk-avatar sdk-avatar-md"><img src="${avatar}" alt="Team member" /></div>`;
260
+ }
261
+ return `<div class="sdk-avatar sdk-avatar-md">${avatar.charAt(0).toUpperCase()}</div>`;
262
+ })
263
+ .join('');
264
+
265
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
266
+ }
267
+
268
+ _formatMessageTime(timestamp) {
269
+ if (!timestamp) return '';
270
+ const date = new Date(timestamp);
271
+ return date.toLocaleTimeString('en-US', {
272
+ hour: 'numeric',
273
+ minute: '2-digit',
274
+ hour12: true,
275
+ });
276
+ }
277
+
278
+ _formatMessageContent(content) {
279
+ if (!content) return '';
280
+ return content
281
+ .replace(/&/g, '&amp;')
282
+ .replace(/</g, '&lt;')
283
+ .replace(/>/g, '&gt;')
284
+ .replace(/\n/g, '<br>');
285
+ }
286
+
287
+ _appendMessage(message) {
288
+ const messagesContainer = this.element.querySelector(
289
+ '.messenger-chat-messages'
290
+ );
291
+ const emptyState = messagesContainer.querySelector('.messenger-chat-empty');
292
+ if (emptyState) {
293
+ emptyState.remove();
294
+ }
295
+
296
+ const messageHtml = this._renderMessage(message);
297
+ const tempDiv = document.createElement('div');
298
+ tempDiv.innerHTML = messageHtml;
299
+ messagesContainer.appendChild(tempDiv.firstElementChild);
300
+ }
301
+
302
+ _scrollToBottom() {
303
+ const messagesContainer = this.element.querySelector(
304
+ '.messenger-chat-messages'
305
+ );
306
+ if (messagesContainer) {
307
+ setTimeout(() => {
308
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
309
+ }, 50);
310
+ }
311
+ }
312
+
313
+ _updateSendButtonState() {
314
+ const input = this.element.querySelector('.messenger-compose-input');
315
+ const sendBtn = this.element.querySelector('.messenger-compose-send');
316
+ if (input && sendBtn) {
317
+ sendBtn.disabled =
318
+ !input.value.trim() && this._pendingAttachments.length === 0;
319
+ }
320
+ }
321
+
322
+ _renderAttachmentPreviews() {
323
+ const container = this.element.querySelector(
324
+ '.messenger-compose-attachments-preview'
325
+ );
326
+ if (!container) return;
327
+
328
+ if (this._pendingAttachments.length === 0) {
329
+ container.innerHTML = '';
330
+ container.style.display = 'none';
331
+ return;
332
+ }
333
+
334
+ container.style.display = 'flex';
335
+ container.innerHTML = this._pendingAttachments
336
+ .map((att, i) => {
337
+ const isImage = att.type.startsWith('image');
338
+ const thumb = isImage
339
+ ? `<img class="messenger-attachment-thumb" src="${att.preview}" alt="${this._escapeHtml(att.file.name)}" />`
340
+ : `<div class="messenger-attachment-thumb messenger-attachment-file-icon"><iconify-icon icon="ph:file-duotone" width="24" height="24"></iconify-icon></div>`;
341
+ return `
342
+ <div class="messenger-attachment-preview" data-index="${i}">
343
+ ${thumb}
344
+ <button class="messenger-attachment-remove" data-index="${i}" aria-label="Remove">&times;</button>
345
+ </div>
346
+ `;
347
+ })
348
+ .join('');
349
+
350
+ container
351
+ .querySelectorAll('.messenger-attachment-remove')
352
+ .forEach((btn) => {
353
+ btn.addEventListener('click', (e) => {
354
+ const idx = parseInt(e.currentTarget.dataset.index, 10);
355
+ this._pendingAttachments.splice(idx, 1);
356
+ this._renderAttachmentPreviews();
357
+ this._updateSendButtonState();
358
+ });
359
+ });
360
+ }
361
+
362
+ _attachEvents() {
363
+ this.element
364
+ .querySelector('.messenger-back-btn')
365
+ .addEventListener('click', () => {
366
+ this.state.setView('messages');
367
+ });
368
+
369
+ this.element
370
+ .querySelector('.sdk-close-btn')
371
+ .addEventListener('click', () => {
372
+ this.state.setOpen(false);
373
+ });
374
+
375
+ const input = this.element.querySelector('.messenger-compose-input');
376
+ const sendBtn = this.element.querySelector('.messenger-compose-send');
377
+
378
+ if (input && sendBtn) {
379
+ input.addEventListener('input', () => {
380
+ input.style.height = 'auto';
381
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
382
+ this._updateSendButtonState();
383
+
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
+ });
395
+
396
+ sendBtn.addEventListener('click', () => {
397
+ this._sendMessage();
398
+ });
399
+ }
400
+
401
+ const attachBtn = this.element.querySelector('.messenger-compose-attach');
402
+ const fileInput = this.element.querySelector(
403
+ '.messenger-compose-file-input'
404
+ );
405
+
406
+ if (attachBtn && fileInput) {
407
+ attachBtn.addEventListener('click', () => {
408
+ fileInput.click();
409
+ });
410
+
411
+ fileInput.addEventListener('change', (e) => {
412
+ const files = e.target.files;
413
+ if (!files) return;
414
+ Array.from(files).forEach((file) => {
415
+ const reader = new FileReader();
416
+ reader.onload = (ev) => {
417
+ this._pendingAttachments.push({
418
+ file,
419
+ preview: ev.target.result,
420
+ type: file.type,
421
+ });
422
+ this._renderAttachmentPreviews();
423
+ this._updateSendButtonState();
424
+ };
425
+ reader.readAsDataURL(file);
426
+ });
427
+ fileInput.value = '';
428
+ });
429
+ }
430
+
431
+ const messagesContainer = this.element.querySelector(
432
+ '.messenger-chat-messages'
433
+ );
434
+ if (messagesContainer) {
435
+ messagesContainer.addEventListener('click', (e) => {
436
+ const fileLink = e.target.closest('.messenger-message-file');
437
+ if (fileLink) {
438
+ e.preventDefault();
439
+ const url = fileLink.dataset.url;
440
+ const name = fileLink.dataset.name;
441
+ this._downloadFile(url, name);
442
+ return;
443
+ }
444
+
445
+ const img = e.target.closest('.messenger-message-image');
446
+ if (img) {
447
+ const url = img.dataset.url || img.src;
448
+ window.open(url, '_blank');
449
+ }
450
+ });
451
+ }
452
+ }
453
+
454
+ async _downloadFile(url, name) {
455
+ try {
456
+ const response = await fetch(url);
457
+ const blob = await response.blob();
458
+ const blobUrl = URL.createObjectURL(blob);
459
+ const a = document.createElement('a');
460
+ a.href = blobUrl;
461
+ a.download = name || 'download';
462
+ a.style.display = 'none';
463
+ document.body.appendChild(a);
464
+ a.click();
465
+ document.body.removeChild(a);
466
+ URL.revokeObjectURL(blobUrl);
467
+ } catch {
468
+ window.open(url, '_blank');
469
+ }
470
+ }
471
+
472
+ _escapeHtml(text) {
473
+ if (!text) return '';
474
+ return text
475
+ .replace(/&/g, '&amp;')
476
+ .replace(/</g, '&lt;')
477
+ .replace(/>/g, '&gt;')
478
+ .replace(/"/g, '&quot;');
479
+ }
480
+
481
+ async _sendMessage() {
482
+ if (this._isConversationClosed) return;
483
+
484
+ const input = this.element.querySelector('.messenger-compose-input');
485
+ const content = input.value.trim();
486
+ const hasAttachments = this._pendingAttachments.length > 0;
487
+
488
+ if (!content && !hasAttachments) return;
489
+
490
+ this._stopTyping();
491
+
492
+ const attachmentsToSend = [...this._pendingAttachments];
493
+ const isNewConversation = !this.state.activeConversationId;
494
+
495
+ if (isNewConversation) {
496
+ if (this.options.onStartConversation) {
497
+ this.options.onStartConversation(content, attachmentsToSend);
498
+ }
499
+ } else {
500
+ const message = {
501
+ id: 'msg_' + Date.now(),
502
+ content: content,
503
+ isOwn: true,
504
+ isOptimistic: true,
505
+ timestamp: new Date().toISOString(),
506
+ attachments: attachmentsToSend.map((a) => ({
507
+ url: a.preview,
508
+ type: a.type.startsWith('image') ? 'image' : 'file',
509
+ name: a.file.name,
510
+ })),
511
+ };
512
+
513
+ this.state.addMessage(this.state.activeConversationId, message);
514
+
515
+ if (this.options.onSendMessage) {
516
+ this.options.onSendMessage(
517
+ this.state.activeConversationId,
518
+ message,
519
+ attachmentsToSend
520
+ );
521
+ }
522
+ }
523
+
524
+ input.value = '';
525
+ input.style.height = 'auto';
526
+ this._pendingAttachments = [];
527
+ this._renderAttachmentPreviews();
528
+ this._updateSendButtonState();
529
+ }
530
+
531
+ _startTyping() {
532
+ if (this._isConversationClosed) return;
533
+ if (!this._isTyping && this.state.activeConversationId) {
534
+ this._isTyping = true;
535
+ if (this.options.onTyping) {
536
+ this.options.onTyping(this.state.activeConversationId, true);
537
+ }
538
+ }
539
+
540
+ if (this._typingTimeout) {
541
+ clearTimeout(this._typingTimeout);
542
+ }
543
+ this._typingTimeout = setTimeout(() => {
544
+ this._stopTyping();
545
+ }, 3000);
546
+ }
547
+
548
+ _stopTyping() {
549
+ if (this._isTyping && this.state.activeConversationId) {
550
+ this._isTyping = false;
551
+ if (this.options.onTyping) {
552
+ this.options.onTyping(this.state.activeConversationId, false);
553
+ }
554
+ }
555
+ if (this._typingTimeout) {
556
+ clearTimeout(this._typingTimeout);
557
+ this._typingTimeout = null;
558
+ }
559
+ }
560
+
561
+ _showTypingIndicator(userName) {
562
+ if (this._typingIndicator) {
563
+ this._typingIndicator.style.display = 'flex';
564
+ const textEl = this._typingIndicator.querySelector(
565
+ '.messenger-typing-text'
566
+ );
567
+ if (textEl) {
568
+ textEl.textContent = `${userName || 'Support'} is typing...`;
569
+ }
570
+ this._scrollToBottom();
571
+ }
572
+ }
573
+
574
+ _hideTypingIndicator() {
575
+ if (this._typingIndicator) {
576
+ this._typingIndicator.style.display = 'none';
577
+ }
578
+ }
579
+
580
+ destroy() {
581
+ if (this._unsubscribe) {
582
+ this._unsubscribe();
583
+ }
584
+ if (this._typingTimeout) {
585
+ clearTimeout(this._typingTimeout);
586
+ }
587
+ this._stopTyping();
588
+ if (this.element && this.element.parentNode) {
589
+ this.element.parentNode.removeChild(this.element);
590
+ }
591
+ }
592
+ }