@hellotext/hellotext 2.1.51 → 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
+ }
@@ -369,6 +369,10 @@ export default class extends Controller {
369
369
  onMessageReceived(message) {
370
370
  const { id, body, attachments } = message
371
371
 
372
+ if (message.carousel) {
373
+ return this.insertCarouselMessage(message)
374
+ }
375
+
372
376
  const div = document.createElement('div')
373
377
  div.innerHTML = body
374
378
 
@@ -409,6 +413,30 @@ export default class extends Controller {
409
413
  this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
410
414
  }
411
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
+
412
440
  resizeInput() {
413
441
  const maxHeight = 96
414
442
 
@@ -422,6 +450,73 @@ export default class extends Controller {
422
450
  this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
423
451
  }
424
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
+
425
520
  async sendMessage(e) {
426
521
  const formData = new FormData()
427
522
 
@@ -451,14 +546,7 @@ export default class extends Controller {
451
546
  formData.append('session', Hellotext.session)
452
547
  formData.append('locale', Locale.toString())
453
548
 
454
- const element = this.messageTemplateTarget.cloneNode(true)
455
-
456
- element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
457
-
458
- element.classList.add('received')
459
- element.style.removeProperty('display')
460
-
461
- element.setAttribute('data-hellotext--webchat-target', 'message')
549
+ const element = this.buildMessageElement()
462
550
 
463
551
  if (this.inputTarget.value.trim().length > 0) {
464
552
  element.querySelector('[data-body]').innerText = this.inputTarget.value
@@ -470,7 +558,7 @@ export default class extends Controller {
470
558
 
471
559
  if (attachments.length > 0) {
472
560
  attachments.forEach(attachment => {
473
- element.querySelector('[data-attachment-container]').appendChild(attachment)
561
+ element.querySelector('[data-attachment-container]').appendChild(attachment.cloneNode(true))
474
562
  })
475
563
  }
476
564
 
@@ -542,6 +630,20 @@ export default class extends Controller {
542
630
  this.attachmentContainerTarget.style.display = ''
543
631
  }
544
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
+
545
647
  openAttachment() {
546
648
  this.attachmentInputTarget.click()
547
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