@hellotext/hellotext 2.4.3 → 2.4.5

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.
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _dom = require("@floating-ui/dom");
8
8
  var _stimulus = require("@hotwired/stimulus");
9
- var _messages = _interopRequireDefault(require("../api/webchat/messages"));
9
+ var _messages2 = _interopRequireDefault(require("../api/webchat/messages"));
10
10
  var _webchat_channel = _interopRequireDefault(require("../channels/webchat_channel"));
11
11
  var _hellotext = _interopRequireDefault(require("../hellotext"));
12
12
  var _core = require("../core");
@@ -35,6 +35,11 @@ const MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
35
35
  hour: 'numeric',
36
36
  minute: '2-digit'
37
37
  };
38
+ const MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
39
+ const SCROLL_ISOLATION_EVENT_OPTIONS = {
40
+ capture: true,
41
+ passive: true
42
+ };
38
43
  let _default = /*#__PURE__*/function (_Controller) {
39
44
  _inherits(_default, _Controller);
40
45
  var _super = _createSuper(_default);
@@ -45,17 +50,23 @@ let _default = /*#__PURE__*/function (_Controller) {
45
50
  _createClass(_default, [{
46
51
  key: "initialize",
47
52
  value: function initialize() {
48
- this.messagesAPI = new _messages.default(this.idValue);
53
+ this.messagesAPI = new _messages2.default(this.idValue);
49
54
  this.webChatChannel = new _webchat_channel.default(this.idValue, _hellotext.default.session, this.conversationIdValue);
50
55
  this.files = [];
51
56
  this.messageIds = new Set();
57
+ this.catchUpAfterMessageId = null;
58
+ this.fetchingCatchUpMessages = false;
52
59
  this.onMessageReceived = this.onMessageReceived.bind(this);
53
60
  this.onMessageReaction = this.onMessageReaction.bind(this);
54
61
  this.onTypingStart = this.onTypingStart.bind(this);
62
+ this.captureCatchUpCursor = this.captureCatchUpCursor.bind(this);
63
+ this.catchUpMessages = this.catchUpMessages.bind(this);
55
64
  this.onScroll = this.onScroll.bind(this);
56
65
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
57
66
  this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
58
67
  this.broadcastChannel = new BroadcastChannel(`hellotext--webchat--${this.idValue}`);
68
+ this.webChatChannel.onDisconnect(this.captureCatchUpCursor);
69
+ this.webChatChannel.onReconnect(this.catchUpMessages);
59
70
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
60
71
  }
61
72
  }, {
@@ -82,7 +93,10 @@ let _default = /*#__PURE__*/function (_Controller) {
82
93
  this.webChatChannel.onMessage(this.onMessageReceived);
83
94
  this.webChatChannel.onTypingStart(this.onTypingStart);
84
95
  this.webChatChannel.onReaction(this.onMessageReaction);
96
+ this.setupMessagesContainerScrollIsolation();
85
97
  this.messagesContainerTarget.addEventListener('scroll', this.onScroll);
98
+ this.messagesContainerTarget.addEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
99
+ this.messagesContainerTarget.addEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
86
100
  if (this.shouldOpenOnMount) {
87
101
  this.openValue = true;
88
102
  }
@@ -101,6 +115,8 @@ let _default = /*#__PURE__*/function (_Controller) {
101
115
  this.teardownOpeningSequence();
102
116
  this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
103
117
  this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
118
+ this.messagesContainerTarget.removeEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
119
+ this.messagesContainerTarget.removeEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
104
120
  window.removeEventListener('keydown', this.closePopoverOnEscape, true);
105
121
 
106
122
  // Clean up typing indicator timeouts
@@ -109,6 +125,21 @@ let _default = /*#__PURE__*/function (_Controller) {
109
125
  this.floatingUICleanup();
110
126
  _get(_getPrototypeOf(_default.prototype), "disconnect", this).call(this);
111
127
  }
128
+ }, {
129
+ key: "setupMessagesContainerScrollIsolation",
130
+ value: function setupMessagesContainerScrollIsolation() {
131
+ this.messagesContainerTarget.style.overscrollBehavior = 'contain';
132
+ this.messagesContainerTarget.style.webkitOverflowScrolling = 'touch';
133
+ this.messagesContainerTarget.style.touchAction = 'pan-y';
134
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent', '');
135
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-wheel', '');
136
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-touch', '');
137
+ }
138
+ }, {
139
+ key: "stopHostScrollPropagation",
140
+ value: function stopHostScrollPropagation(event) {
141
+ event.stopPropagation();
142
+ }
112
143
  }, {
113
144
  key: "onTypingStart",
114
145
  value: function onTypingStart() {
@@ -237,7 +268,12 @@ let _default = /*#__PURE__*/function (_Controller) {
237
268
  const div = document.createElement('div');
238
269
  div.innerHTML = body;
239
270
  const element = this.messageTemplateTarget.cloneNode(true);
271
+ element.classList.add('hellotext--webchat-message');
240
272
  element.setAttribute('data-hellotext--webchat-target', 'message');
273
+ element.setAttribute('data-id', message.id);
274
+ if (createdAt) {
275
+ element.setAttribute('data-created-at', createdAt);
276
+ }
241
277
  element.style.removeProperty('display');
242
278
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
243
279
  if (message.state === 'received') {
@@ -307,7 +343,7 @@ let _default = /*#__PURE__*/function (_Controller) {
307
343
  this.popoverTarget.classList.remove(...this.fadeOutClasses);
308
344
  (_this$dismissTeaserFo = this.dismissTeaserForSession) === null || _this$dismissTeaserFo === void 0 ? void 0 : _this$dismissTeaserFo.call(this);
309
345
  if (!this.onMobile) {
310
- this.inputTarget.focus();
346
+ this.focusComposeInput();
311
347
  }
312
348
  if (!this.scrolled) {
313
349
  requestAnimationFrame(() => {
@@ -362,7 +398,7 @@ let _default = /*#__PURE__*/function (_Controller) {
362
398
  }
363
399
  }, {
364
400
  key: "onMessageReceived",
365
- value: function onMessageReceived(message) {
401
+ value: function onMessageReceived(message, options = {}) {
366
402
  var _this$hideTeaser;
367
403
  const {
368
404
  id,
@@ -374,15 +410,17 @@ let _default = /*#__PURE__*/function (_Controller) {
374
410
  if (!this.claimMessageId(id)) return;
375
411
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
376
412
  if (message.carousel) {
377
- return this.insertCarouselMessage(message);
413
+ return this.insertCarouselMessage(message, options);
378
414
  }
379
415
  const div = document.createElement('div');
380
416
  div.innerHTML = body;
381
417
  const element = this.messageTemplateTarget.cloneNode(true);
418
+ element.classList.add('hellotext--webchat-message');
382
419
  element.style.display = 'flex';
383
420
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
384
421
  element.setAttribute('data-id', id);
385
422
  element.setAttribute('data-hellotext--webchat-target', 'message');
423
+ this.setMessageCreatedAt(element, createdAt);
386
424
  this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
387
425
  if (attachments) {
388
426
  attachments.forEach(attachmentUrl => {
@@ -394,14 +432,16 @@ let _default = /*#__PURE__*/function (_Controller) {
394
432
  });
395
433
  }
396
434
  this.clearTypingIndicator();
397
- this.messagesContainerTarget.appendChild(element);
435
+ this.insertMessageElement(element);
398
436
  _hellotext.default.eventEmitter.dispatch('webchat:message:received', {
399
437
  ...message,
400
438
  body: element.querySelector('[data-body]').innerText
401
439
  });
402
- element.scrollIntoView({
403
- behavior: 'smooth'
404
- });
440
+ if (options.scroll !== false) {
441
+ element.scrollIntoView({
442
+ behavior: 'smooth'
443
+ });
444
+ }
405
445
  this.updateMessageTeaser(teaser);
406
446
  if (this.openValue) {
407
447
  this.messagesAPI.markAsSeen(id);
@@ -421,6 +461,70 @@ let _default = /*#__PURE__*/function (_Controller) {
421
461
  this.messageIds.add(id);
422
462
  return !messageTargets.some(element => element.dataset.id === id);
423
463
  }
464
+ }, {
465
+ key: "captureCatchUpCursor",
466
+ value: function captureCatchUpCursor() {
467
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
468
+ }
469
+ }, {
470
+ key: "catchUpMessages",
471
+ value: async function catchUpMessages() {
472
+ const afterId = this.catchUpAfterMessageId;
473
+ if (!afterId || this.fetchingCatchUpMessages) return;
474
+ this.fetchingCatchUpMessages = true;
475
+ try {
476
+ const response = await this.messagesAPI.catchUp(afterId);
477
+ const {
478
+ messages = []
479
+ } = await response.json();
480
+ messages.forEach(message => this.onMessageReceived(message, {
481
+ scroll: false
482
+ }));
483
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
484
+ } finally {
485
+ this.fetchingCatchUpMessages = false;
486
+ }
487
+ }
488
+ }, {
489
+ key: "lastRenderedMessageId",
490
+ get: function () {
491
+ var _messages;
492
+ const messages = this.persistedMessageElements;
493
+ return ((_messages = messages[messages.length - 1]) === null || _messages === void 0 ? void 0 : _messages.dataset.id) || null;
494
+ }
495
+ }, {
496
+ key: "persistedMessageElements",
497
+ get: function () {
498
+ return Array.from(this.messagesContainerTarget.querySelectorAll('.hellotext--webchat-message[data-id]'));
499
+ }
500
+ }, {
501
+ key: "setMessageCreatedAt",
502
+ value: function setMessageCreatedAt(element, createdAt) {
503
+ if (createdAt) {
504
+ element.setAttribute('data-created-at', createdAt);
505
+ }
506
+ }
507
+ }, {
508
+ key: "insertMessageElement",
509
+ value: function insertMessageElement(element) {
510
+ const nextElement = this.nextMessageElementFor(element);
511
+ if (nextElement) {
512
+ this.messagesContainerTarget.insertBefore(element, nextElement);
513
+ } else {
514
+ this.messagesContainerTarget.appendChild(element);
515
+ }
516
+ }
517
+ }, {
518
+ key: "nextMessageElementFor",
519
+ value: function nextMessageElementFor(element) {
520
+ const createdAt = Date.parse(element.dataset.createdAt);
521
+ if (Number.isNaN(createdAt)) return null;
522
+ return this.persistedMessageElements.find(messageElement => {
523
+ if (messageElement === element) return false;
524
+ const messageCreatedAt = Date.parse(messageElement.dataset.createdAt);
525
+ return !Number.isNaN(messageCreatedAt) && messageCreatedAt > createdAt;
526
+ });
527
+ }
424
528
  }, {
425
529
  key: "updateMessageTeaser",
426
530
  value: function updateMessageTeaser(teaser) {
@@ -435,18 +539,23 @@ let _default = /*#__PURE__*/function (_Controller) {
435
539
  }
436
540
  }, {
437
541
  key: "insertCarouselMessage",
438
- value: function insertCarouselMessage(message) {
542
+ value: function insertCarouselMessage(message, options = {}) {
439
543
  var _element$querySelecto;
440
544
  const html = message.html;
545
+ const createdAt = message.created_at || message.createdAt;
441
546
  const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
547
+ element.classList.add('hellotext--webchat-message');
442
548
  element.setAttribute('data-id', message.id);
443
549
  element.setAttribute('data-hellotext--webchat-target', 'message');
550
+ this.setMessageCreatedAt(element, createdAt);
444
551
  this.localizeMessageTimestamps(element);
445
552
  this.clearTypingIndicator();
446
- this.messagesContainerTarget.appendChild(element);
447
- element.scrollIntoView({
448
- behavior: 'smooth'
449
- });
553
+ this.insertMessageElement(element);
554
+ if (options.scroll !== false) {
555
+ element.scrollIntoView({
556
+ behavior: 'smooth'
557
+ });
558
+ }
450
559
  _hellotext.default.eventEmitter.dispatch('webchat:message:received', {
451
560
  ...message,
452
561
  body: ((_element$querySelecto = element.querySelector('[data-body]')) === null || _element$querySelecto === void 0 ? void 0 : _element$querySelecto.innerText) || ''
@@ -665,7 +774,7 @@ let _default = /*#__PURE__*/function (_Controller) {
665
774
  this.attachmentContainerTarget.innerHTML = '';
666
775
  this.attachmentContainerTarget.style.display = 'none';
667
776
  this.errorMessageContainerTarget.style.display = 'none';
668
- this.inputTarget.focus();
777
+ this.focusComposeInput();
669
778
 
670
779
  // Set up optimistic typing indicator BEFORE making the API call
671
780
  // This prevents race conditions with server responses
@@ -718,12 +827,10 @@ let _default = /*#__PURE__*/function (_Controller) {
718
827
  } = event;
719
828
  const ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
720
829
  if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
830
+ if (!this.focusComposeInput({
831
+ moveCursorToEnd: true
832
+ })) return;
721
833
  event.preventDefault();
722
- this.inputTarget.focus();
723
- if (typeof this.inputTarget.selectionStart === 'number') {
724
- const position = this.inputTarget.value.length;
725
- this.inputTarget.setSelectionRange(position, position);
726
- }
727
834
  }
728
835
  }, {
729
836
  key: "closePopoverFromHeader",
@@ -872,7 +979,7 @@ let _default = /*#__PURE__*/function (_Controller) {
872
979
  this.files = [...this.files, ...newFiles];
873
980
  this.errorMessageContainerTarget.innerText = '';
874
981
  newFiles.forEach(file => this.createAttachmentElement(file));
875
- this.inputTarget.focus();
982
+ this.focusComposeInput();
876
983
  }
877
984
  }, {
878
985
  key: "createAttachmentElement",
@@ -907,7 +1014,7 @@ let _default = /*#__PURE__*/function (_Controller) {
907
1014
  this.files = this.files.filter(file => file.name !== attachment.dataset.name);
908
1015
  this.attachmentInputTarget.value = '';
909
1016
  attachment.remove();
910
- this.inputTarget.focus();
1017
+ this.focusComposeInput();
911
1018
  }
912
1019
  }, {
913
1020
  key: "attachmentTargetDisconnected",
@@ -936,7 +1043,22 @@ let _default = /*#__PURE__*/function (_Controller) {
936
1043
  const end = this.inputTarget.selectionEnd;
937
1044
  this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end);
938
1045
  this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
1046
+ this.focusComposeInput();
1047
+ }
1048
+ }, {
1049
+ key: "focusComposeInput",
1050
+ value: function focusComposeInput({
1051
+ moveCursorToEnd = false
1052
+ } = {}) {
1053
+ if (!this.shouldAutofocusCompose) return false;
1054
+ if (this.hasInputTarget === false) return false;
1055
+ if (this.hasInputTarget === undefined && !this.inputTarget) return false;
939
1056
  this.inputTarget.focus();
1057
+ if (moveCursorToEnd && typeof this.inputTarget.selectionStart === 'number') {
1058
+ const position = this.inputTarget.value.length;
1059
+ this.inputTarget.setSelectionRange(position, position);
1060
+ }
1061
+ return true;
940
1062
  }
941
1063
  }, {
942
1064
  key: "byteToMegabyte",
@@ -955,9 +1077,32 @@ let _default = /*#__PURE__*/function (_Controller) {
955
1077
  get: function () {
956
1078
  return localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened' && !this.onMobile;
957
1079
  }
1080
+ }, {
1081
+ key: "shouldAutofocusCompose",
1082
+ get: function () {
1083
+ return !this.usesVirtualKeyboard;
1084
+ }
1085
+ }, {
1086
+ key: "usesVirtualKeyboard",
1087
+ get: function () {
1088
+ var _navigator$userAgentD;
1089
+ if (typeof navigator === 'undefined') return false;
1090
+ const userAgent = navigator.userAgent || '';
1091
+ const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
1092
+ const isMobileUserAgent = MOBILE_USER_AGENT_PATTERN.test(userAgent);
1093
+ const isUserAgentDataMobile = ((_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.mobile) === true;
1094
+ return isMobileUserAgent || isIPadOS || isUserAgentDataMobile || this.hasTouchOnlyPointer;
1095
+ }
1096
+ }, {
1097
+ key: "hasTouchOnlyPointer",
1098
+ get: function () {
1099
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1100
+ return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(hover: none)').matches;
1101
+ }
958
1102
  }, {
959
1103
  key: "onMobile",
960
1104
  get: function () {
1105
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
961
1106
  return window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches;
962
1107
  }
963
1108
  }], [{