@hellotext/hellotext 2.1.6 → 2.2.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.
@@ -0,0 +1,95 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ id: String,
6
+ }
7
+
8
+ static targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard']
9
+
10
+ connect() {
11
+ this.updateFades()
12
+ }
13
+
14
+ setId({ detail: id }) {
15
+ this.idValue = id
16
+ this.element.id = id
17
+ }
18
+
19
+ onScroll() {
20
+ this.updateFades()
21
+ }
22
+
23
+ quickReply({ currentTarget }) {
24
+ const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
25
+
26
+ this.dispatch('quickReply', {
27
+ detail: {
28
+ id: this.idValue,
29
+ product: card.dataset.id,
30
+ buttonId: currentTarget.dataset.id,
31
+ body: currentTarget.dataset.text,
32
+ cardElement: card,
33
+ },
34
+ })
35
+ }
36
+
37
+ moveToLeft() {
38
+ if (!this.hasCarouselContainerTarget) return
39
+
40
+ const scrollAmount = this.getScrollAmount()
41
+
42
+ this.carouselContainerTarget.scrollBy({
43
+ left: -scrollAmount,
44
+ behavior: 'smooth',
45
+ })
46
+ }
47
+
48
+ moveToRight() {
49
+ if (!this.hasCarouselContainerTarget) return
50
+
51
+ const scrollAmount = this.getScrollAmount()
52
+
53
+ this.carouselContainerTarget.scrollBy({
54
+ left: scrollAmount,
55
+ behavior: 'smooth',
56
+ })
57
+ }
58
+
59
+ getScrollAmount() {
60
+ // Get the actual card width from DOM
61
+ const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card')
62
+
63
+ if (!firstCard) {
64
+ return 280 // Fallback to default desktop card width
65
+ }
66
+
67
+ const cardWidth = firstCard.offsetWidth
68
+ const gap = 16 // gap-x-4 = 1rem = 16px
69
+
70
+ return cardWidth + gap
71
+ }
72
+
73
+ updateFades() {
74
+ if (!this.hasCarouselContainerTarget) return
75
+
76
+ const scrollLeft = this.carouselContainerTarget.scrollLeft
77
+ const maxScroll =
78
+ this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth
79
+
80
+ // Show left fade if scrolled past start
81
+ if (scrollLeft > 0) {
82
+ this.leftFadeTarget.classList.remove('hidden')
83
+ } else {
84
+ this.leftFadeTarget.classList.add('hidden')
85
+ }
86
+
87
+ // Show right fade if not at end
88
+ if (scrollLeft < maxScroll - 1) {
89
+ // -1 for rounding errors
90
+ this.rightFadeTarget.classList.remove('hidden')
91
+ } else {
92
+ this.rightFadeTarget.classList.add('hidden')
93
+ }
94
+ }
95
+ }
@@ -309,6 +309,8 @@ export default class extends Controller {
309
309
  }
310
310
 
311
311
  onPopoverOpened() {
312
+ this.popoverTarget.classList.remove(...this.fadeOutClasses)
313
+
312
314
  if (!this.onMobile) {
313
315
  this.inputTarget.focus()
314
316
  }
@@ -367,6 +369,10 @@ export default class extends Controller {
367
369
  onMessageReceived(message) {
368
370
  const { id, body, attachments } = message
369
371
 
372
+ if (message.carousel) {
373
+ return this.insertCarouselMessage(message)
374
+ }
375
+
370
376
  const div = document.createElement('div')
371
377
  div.innerHTML = body
372
378
 
@@ -407,6 +413,30 @@ export default class extends Controller {
407
413
  this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
408
414
  }
409
415
 
416
+ insertCarouselMessage(message) {
417
+ const html = message.html
418
+ const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
419
+
420
+ this.clearTypingIndicator()
421
+ this.messagesContainerTarget.appendChild(element)
422
+
423
+ element.scrollIntoView({ behavior: 'smooth' })
424
+
425
+ Hellotext.eventEmitter.dispatch('webchat:message:received', {
426
+ ...message,
427
+ body: element.querySelector('[data-body]')?.innerText || '',
428
+ })
429
+
430
+ if (this.openValue) {
431
+ this.messagesAPI.markAsSeen(message.id)
432
+ return
433
+ }
434
+
435
+ this.unreadCounterTarget.style.display = 'flex'
436
+ const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
437
+ this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
438
+ }
439
+
410
440
  resizeInput() {
411
441
  const maxHeight = 96
412
442
 
@@ -420,6 +450,73 @@ export default class extends Controller {
420
450
  this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
421
451
  }
422
452
 
453
+ async sendQuickReplyMessage({ detail: { id, product, buttonId, body, cardElement } }) {
454
+ const formData = new FormData()
455
+
456
+ formData.append('message[body]', body)
457
+ formData.append('message[replied_to]', id)
458
+ formData.append('message[product]', product)
459
+ formData.append('message[button]', buttonId)
460
+
461
+ formData.append('session', Hellotext.session)
462
+ formData.append('locale', Locale.toString())
463
+
464
+ const element = this.buildMessageElement()
465
+ const attachment = cardElement.querySelector('img')?.cloneNode(true)
466
+
467
+ element.querySelector('[data-body]').innerText = body
468
+
469
+ if (attachment) {
470
+ attachment.removeAttribute('width')
471
+ attachment.removeAttribute('height')
472
+
473
+ element.querySelector('[data-attachment-container]').appendChild(attachment)
474
+ }
475
+
476
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
477
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
478
+ } else {
479
+ this.messagesContainerTarget.appendChild(element)
480
+ }
481
+
482
+ element.scrollIntoView({ behavior: 'smooth' })
483
+
484
+ this.broadcastChannel.postMessage({
485
+ type: 'message:sent',
486
+ element: element.outerHTML,
487
+ })
488
+
489
+ const response = await this.messagesAPI.create(formData)
490
+
491
+ if (response.failed) {
492
+ // Clear the optimistic typing indicator on failure
493
+ clearTimeout(this.optimisticTypingTimeout)
494
+
495
+ this.broadcastChannel.postMessage({
496
+ type: 'message:failed',
497
+ id: element.id,
498
+ })
499
+
500
+ return element.classList.add('failed')
501
+ }
502
+
503
+ const data = await response.json()
504
+
505
+ this.dispatch('set:id', { target: element, detail: data.id })
506
+
507
+ const message = {
508
+ id: data.id,
509
+ body: body,
510
+ attachments: attachment ? [attachment.src] : [],
511
+ replied_to: id,
512
+ product: product,
513
+ button: buttonId,
514
+ type: 'quick_reply',
515
+ }
516
+
517
+ Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
518
+ }
519
+
423
520
  async sendMessage(e) {
424
521
  const formData = new FormData()
425
522
 
@@ -449,14 +546,7 @@ export default class extends Controller {
449
546
  formData.append('session', Hellotext.session)
450
547
  formData.append('locale', Locale.toString())
451
548
 
452
- const element = this.messageTemplateTarget.cloneNode(true)
453
-
454
- element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
455
-
456
- element.classList.add('received')
457
- element.style.removeProperty('display')
458
-
459
- element.setAttribute('data-hellotext--webchat-target', 'message')
549
+ const element = this.buildMessageElement()
460
550
 
461
551
  if (this.inputTarget.value.trim().length > 0) {
462
552
  element.querySelector('[data-body]').innerText = this.inputTarget.value
@@ -468,7 +558,7 @@ export default class extends Controller {
468
558
 
469
559
  if (attachments.length > 0) {
470
560
  attachments.forEach(attachment => {
471
- element.querySelector('[data-attachment-container]').appendChild(attachment)
561
+ element.querySelector('[data-attachment-container]').appendChild(attachment.cloneNode(true))
472
562
  })
473
563
  }
474
564
 
@@ -540,6 +630,20 @@ export default class extends Controller {
540
630
  this.attachmentContainerTarget.style.display = ''
541
631
  }
542
632
 
633
+ buildMessageElement() {
634
+ const element = this.messageTemplateTarget.cloneNode(true)
635
+
636
+ element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
637
+
638
+ element.classList.add('received')
639
+ element.style.removeProperty('display')
640
+
641
+ element.setAttribute('data-controller', 'hellotext--message')
642
+ element.setAttribute('data-hellotext--webchat-target', 'message')
643
+
644
+ return element
645
+ }
646
+
543
647
  openAttachment() {
544
648
  this.attachmentInputTarget.click()
545
649
  }
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import { Application } from '@hotwired/stimulus'
2
2
  import Hellotext from './hellotext'
3
3
 
4
4
  import FormController from './controllers/form_controller'
5
+ import MessageController from './controllers/message_controller'
5
6
  import WebChatEmojiController from './controllers/webchat/emoji_picker_controller'
6
7
  import WebchatController from './controllers/webchat_controller'
7
8
 
@@ -10,6 +11,7 @@ const application = Application.start()
10
11
  application.register('hellotext--form', FormController)
11
12
  application.register('hellotext--webchat', WebchatController)
12
13
  application.register('hellotext--webchat--emoji', WebChatEmojiController)
14
+ application.register('hellotext--message', MessageController)
13
15
 
14
16
  window.Hellotext = Hellotext
15
17