@hellotext/hellotext 2.4.2 → 2.4.4

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.
@@ -28,6 +28,15 @@ import { usePopover } from './mixins/usePopover';
28
28
  import { useBehaviour } from './webchat/useBehaviour';
29
29
  import { useOpeningSequence } from './webchat/useOpeningSequence';
30
30
  import { useTeaser } from './webchat/useTeaser';
31
+ var POPOVER_ANIMATION_DURATION = 120;
32
+ var MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
33
+ hour: 'numeric',
34
+ minute: '2-digit'
35
+ };
36
+ var SCROLL_ISOLATION_EVENT_OPTIONS = {
37
+ capture: true,
38
+ passive: true
39
+ };
31
40
  var _default = /*#__PURE__*/function (_Controller) {
32
41
  _inherits(_default, _Controller);
33
42
  var _super = _createSuper(_default);
@@ -47,6 +56,7 @@ var _default = /*#__PURE__*/function (_Controller) {
47
56
  this.onTypingStart = this.onTypingStart.bind(this);
48
57
  this.onScroll = this.onScroll.bind(this);
49
58
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
59
+ this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
50
60
  this.broadcastChannel = new BroadcastChannel("hellotext--webchat--".concat(this.idValue));
51
61
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
52
62
  }
@@ -70,15 +80,20 @@ var _default = /*#__PURE__*/function (_Controller) {
70
80
  }
71
81
  this.setupTeaser();
72
82
  this.setupOpeningSequence();
83
+ this.localizeMessageTimestamps();
73
84
  this.webChatChannel.onMessage(this.onMessageReceived);
74
85
  this.webChatChannel.onTypingStart(this.onTypingStart);
75
86
  this.webChatChannel.onReaction(this.onMessageReaction);
87
+ this.setupMessagesContainerScrollIsolation();
76
88
  this.messagesContainerTarget.addEventListener('scroll', this.onScroll);
89
+ this.messagesContainerTarget.addEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
90
+ this.messagesContainerTarget.addEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
77
91
  if (this.shouldOpenOnMount) {
78
92
  this.openValue = true;
79
93
  }
80
94
  Hellotext.eventEmitter.dispatch('webchat:mounted');
81
95
  this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent);
96
+ window.addEventListener('keydown', this.closePopoverOnEscape, true);
82
97
  this.scheduleBehaviourOpen();
83
98
  _get(_getPrototypeOf(_default.prototype), "connect", this).call(this);
84
99
  }
@@ -86,10 +101,14 @@ var _default = /*#__PURE__*/function (_Controller) {
86
101
  key: "disconnect",
87
102
  value: function disconnect() {
88
103
  this.cancelBehaviourOpen();
104
+ this.clearPopoverOpenAnimation();
89
105
  this.teardownTeaser();
90
106
  this.teardownOpeningSequence();
91
107
  this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
92
108
  this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
109
+ this.messagesContainerTarget.removeEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
110
+ this.messagesContainerTarget.removeEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
111
+ window.removeEventListener('keydown', this.closePopoverOnEscape, true);
93
112
 
94
113
  // Clean up typing indicator timeouts
95
114
  this.clearTypingIndicator();
@@ -97,6 +116,21 @@ var _default = /*#__PURE__*/function (_Controller) {
97
116
  this.floatingUICleanup();
98
117
  _get(_getPrototypeOf(_default.prototype), "disconnect", this).call(this);
99
118
  }
119
+ }, {
120
+ key: "setupMessagesContainerScrollIsolation",
121
+ value: function setupMessagesContainerScrollIsolation() {
122
+ this.messagesContainerTarget.style.overscrollBehavior = 'contain';
123
+ this.messagesContainerTarget.style.webkitOverflowScrolling = 'touch';
124
+ this.messagesContainerTarget.style.touchAction = 'pan-y';
125
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent', '');
126
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-wheel', '');
127
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-touch', '');
128
+ }
129
+ }, {
130
+ key: "stopHostScrollPropagation",
131
+ value: function stopHostScrollPropagation(event) {
132
+ event.stopPropagation();
133
+ }
100
134
  }, {
101
135
  key: "onTypingStart",
102
136
  value: function onTypingStart() {
@@ -178,6 +212,7 @@ var _default = /*#__PURE__*/function (_Controller) {
178
212
  var callbacks = {
179
213
  'message:sent': data => {
180
214
  var element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
215
+ this.localizeMessageTimestamps(element);
181
216
 
182
217
  // Insert message before typing indicator if one exists
183
218
  if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
@@ -190,8 +225,8 @@ var _default = /*#__PURE__*/function (_Controller) {
190
225
  });
191
226
  },
192
227
  'message:failed': data => {
193
- var _this$messagesContain;
194
- (_this$messagesContain = this.messagesContainerTarget.querySelector("#".concat(data.id))) === null || _this$messagesContain === void 0 ? void 0 : _this$messagesContain.classList.add('failed');
228
+ var element = this.messagesContainerTarget.querySelector("#".concat(data.id));
229
+ this.markMessageFailed(element, data.reason);
195
230
  }
196
231
  };
197
232
  if (callbacks[data.type]) {
@@ -221,6 +256,7 @@ var _default = /*#__PURE__*/function (_Controller) {
221
256
  body,
222
257
  attachments
223
258
  } = message;
259
+ var createdAt = message.created_at || message.createdAt;
224
260
  var div = document.createElement('div');
225
261
  div.innerHTML = body;
226
262
  var element = this.messageTemplateTarget.cloneNode(true);
@@ -243,6 +279,7 @@ var _default = /*#__PURE__*/function (_Controller) {
243
279
  });
244
280
  }
245
281
  element.setAttribute('data-body', body);
282
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
246
283
  this.messagesContainerTarget.prepend(element);
247
284
  });
248
285
  this.messagesContainerTarget.scroll({
@@ -266,10 +303,30 @@ var _default = /*#__PURE__*/function (_Controller) {
266
303
  }, {
267
304
  key: "closePopover",
268
305
  value: function closePopover() {
269
- this.popoverTarget.classList.add(...this.fadeOutClasses);
270
- setTimeout(() => {
271
- this.openValue = false;
272
- }, 250);
306
+ this.clearPopoverOpenAnimation();
307
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
308
+ this.openValue = false;
309
+ }
310
+ }, {
311
+ key: "preparePopoverOpenAnimation",
312
+ value: function preparePopoverOpenAnimation() {
313
+ this.clearPopoverOpenAnimation();
314
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
315
+ this.popoverTarget.classList.add('hellotext--webchat-popover-opening');
316
+ this.popoverOpenAnimationTimeout = setTimeout(() => {
317
+ this.popoverTarget.classList.remove('hellotext--webchat-popover-opening');
318
+ this.popoverOpenAnimationTimeout = null;
319
+ }, POPOVER_ANIMATION_DURATION);
320
+ }
321
+ }, {
322
+ key: "clearPopoverOpenAnimation",
323
+ value: function clearPopoverOpenAnimation() {
324
+ var _this$popoverTarget;
325
+ if (this.popoverOpenAnimationTimeout) {
326
+ clearTimeout(this.popoverOpenAnimationTimeout);
327
+ this.popoverOpenAnimationTimeout = null;
328
+ }
329
+ (_this$popoverTarget = this.popoverTarget) === null || _this$popoverTarget === void 0 ? void 0 : _this$popoverTarget.classList.remove('hellotext--webchat-popover-opening');
273
330
  }
274
331
  }, {
275
332
  key: "onPopoverOpened",
@@ -303,6 +360,7 @@ var _default = /*#__PURE__*/function (_Controller) {
303
360
  }, {
304
361
  key: "onPopoverClosed",
305
362
  value: function onPopoverClosed() {
363
+ this.clearPopoverOpenAnimation();
306
364
  Hellotext.eventEmitter.dispatch('webchat:closed');
307
365
  localStorage.setItem("hellotext--webchat--".concat(this.idValue), 'closed');
308
366
  }
@@ -340,6 +398,7 @@ var _default = /*#__PURE__*/function (_Controller) {
340
398
  attachments,
341
399
  teaser
342
400
  } = message;
401
+ var createdAt = message.created_at || message.createdAt;
343
402
  if (!this.claimMessageId(id)) return;
344
403
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
345
404
  if (message.carousel) {
@@ -352,6 +411,7 @@ var _default = /*#__PURE__*/function (_Controller) {
352
411
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
353
412
  element.setAttribute('data-id', id);
354
413
  element.setAttribute('data-hellotext--webchat-target', 'message');
414
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
355
415
  if (attachments) {
356
416
  attachments.forEach(attachmentUrl => {
357
417
  var _this$messageAttachme2;
@@ -408,6 +468,7 @@ var _default = /*#__PURE__*/function (_Controller) {
408
468
  var element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
409
469
  element.setAttribute('data-id', message.id);
410
470
  element.setAttribute('data-hellotext--webchat-target', 'message');
471
+ this.localizeMessageTimestamps(element);
411
472
  this.clearTypingIndicator();
412
473
  this.messagesContainerTarget.appendChild(element);
413
474
  element.scrollIntoView({
@@ -453,14 +514,14 @@ var _default = /*#__PURE__*/function (_Controller) {
453
514
  (_this$dismissTeaserFo2 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo2 === void 0 ? void 0 : _this$dismissTeaserFo2.call(this);
454
515
  var formData = new FormData();
455
516
  formData.append('message[body]', body);
456
- formData.append('message[replied_to]', id);
457
- formData.append('message[product]', product);
458
- formData.append('message[button]', buttonId);
517
+ if (id) formData.append('message[replied_to]', id);
518
+ if (product) formData.append('message[product]', product);
519
+ if (buttonId) formData.append('message[button]', buttonId);
459
520
  formData.append('session', Hellotext.session);
460
521
  formData.append('locale', Locale.toString());
461
522
  this.appendOpeningSequenceMessageIds(formData);
462
523
  var element = this.buildMessageElement();
463
- var attachment = (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
524
+ var attachment = cardElement === null || cardElement === void 0 ? void 0 : (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
464
525
  element.querySelector('[data-body]').innerText = body;
465
526
  if (attachment) {
466
527
  var _this$messageAttachme3;
@@ -484,17 +545,14 @@ var _default = /*#__PURE__*/function (_Controller) {
484
545
  if (response.failed) {
485
546
  // Clear the optimistic typing indicator on failure
486
547
  clearTimeout(this.optimisticTypingTimeout);
487
- this.broadcastChannel.postMessage({
488
- type: 'message:failed',
489
- id: element.id
490
- });
491
- return element.classList.add('failed');
548
+ return this.markMessageFailedFromResponse(response, element);
492
549
  }
493
550
  var data = yield response.json();
494
551
  this.dispatch('set:id', {
495
552
  target: element,
496
553
  detail: data.id
497
554
  });
555
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
498
556
  this.clearRevealedOpeningSequenceMessageIds();
499
557
  var message = {
500
558
  id: data.id,
@@ -555,14 +613,11 @@ var _default = /*#__PURE__*/function (_Controller) {
555
613
  var response = yield this.messagesAPI.create(formData);
556
614
  if (response.failed) {
557
615
  clearTimeout(this.optimisticTypingTimeout);
558
- this.broadcastChannel.postMessage({
559
- type: 'message:failed',
560
- id: element.id
561
- });
562
- return element.classList.add('failed');
616
+ return this.markMessageFailedFromResponse(response, element);
563
617
  }
564
618
  var data = yield response.json();
565
619
  element.setAttribute('data-id', data.id);
620
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
566
621
  this.clearRevealedOpeningSequenceMessageIds();
567
622
  Hellotext.eventEmitter.dispatch('webchat:message:sent', {
568
623
  id: data.id,
@@ -664,15 +719,12 @@ var _default = /*#__PURE__*/function (_Controller) {
664
719
  if (response.failed) {
665
720
  // Clear the optimistic typing indicator on failure
666
721
  clearTimeout(this.optimisticTypingTimeout);
667
- this.broadcastChannel.postMessage({
668
- type: 'message:failed',
669
- id: element.id
670
- });
671
- return element.classList.add('failed');
722
+ return this.markMessageFailedFromResponse(response, element);
672
723
  }
673
724
  var data = yield response.json();
674
725
  element.setAttribute('data-id', data.id);
675
726
  message.id = data.id;
727
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
676
728
  this.clearRevealedOpeningSequenceMessageIds();
677
729
  Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
678
730
  if (data.conversation !== this.conversationIdValue) {
@@ -700,8 +752,145 @@ var _default = /*#__PURE__*/function (_Controller) {
700
752
  element.style.removeProperty('display');
701
753
  element.setAttribute('data-controller', 'hellotext--message');
702
754
  element.setAttribute('data-hellotext--webchat-target', 'message');
755
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date());
703
756
  return element;
704
757
  }
758
+ }, {
759
+ key: "focusCompose",
760
+ value: function focusCompose(event) {
761
+ var {
762
+ target
763
+ } = event;
764
+ var ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
765
+ if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
766
+ event.preventDefault();
767
+ this.inputTarget.focus();
768
+ if (typeof this.inputTarget.selectionStart === 'number') {
769
+ var position = this.inputTarget.value.length;
770
+ this.inputTarget.setSelectionRange(position, position);
771
+ }
772
+ }
773
+ }, {
774
+ key: "closePopoverFromHeader",
775
+ value: function closePopoverFromHeader(event) {
776
+ var {
777
+ target
778
+ } = event;
779
+ if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return;
780
+ event.preventDefault();
781
+ this.closePopover();
782
+ }
783
+ }, {
784
+ key: "closePopoverOnEscape",
785
+ value: function closePopoverOnEscape(event) {
786
+ var _this$triggerTarget, _this$triggerTarget$f;
787
+ if (event.key !== 'Escape' || !this.openValue) return;
788
+ event.preventDefault();
789
+ event.stopPropagation();
790
+ this.closePopover();
791
+ (_this$triggerTarget = this.triggerTarget) === null || _this$triggerTarget === void 0 ? void 0 : (_this$triggerTarget$f = _this$triggerTarget.focus) === null || _this$triggerTarget$f === void 0 ? void 0 : _this$triggerTarget$f.call(_this$triggerTarget);
792
+ }
793
+ }, {
794
+ key: "markMessageFailedFromResponse",
795
+ value: function () {
796
+ var _markMessageFailedFromResponse = _asyncToGenerator(function* (response, element) {
797
+ var reason = yield this.messageFailureReason(response);
798
+ this.markMessageFailed(element, reason);
799
+ this.broadcastChannel.postMessage({
800
+ type: 'message:failed',
801
+ id: element.id,
802
+ reason
803
+ });
804
+ });
805
+ function markMessageFailedFromResponse(_x4, _x5) {
806
+ return _markMessageFailedFromResponse.apply(this, arguments);
807
+ }
808
+ return markMessageFailedFromResponse;
809
+ }()
810
+ }, {
811
+ key: "markMessageFailed",
812
+ value: function markMessageFailed(element, reason) {
813
+ if (!element) return;
814
+ element.classList.add('failed');
815
+ if (!reason) return;
816
+ var timestamp = element.querySelector('[data-message-timestamp]');
817
+ if (timestamp) {
818
+ timestamp.textContent = reason;
819
+ }
820
+ }
821
+ }, {
822
+ key: "localizeMessageTimestamps",
823
+ value: function localizeMessageTimestamps() {
824
+ var _root$matches, _root$querySelectorAl;
825
+ var root = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.element;
826
+ if (!root) return;
827
+ var timestamps = (_root$matches = root.matches) !== null && _root$matches !== void 0 && _root$matches.call(root, 'time[datetime][data-message-timestamp]') ? [root] : Array.from(((_root$querySelectorAl = root.querySelectorAll) === null || _root$querySelectorAl === void 0 ? void 0 : _root$querySelectorAl.call(root, 'time[datetime][data-message-timestamp]')) || []);
828
+ timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp));
829
+ }
830
+ }, {
831
+ key: "localizeMessageTimestamp",
832
+ value: function localizeMessageTimestamp(timestamp) {
833
+ var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : timestamp === null || timestamp === void 0 ? void 0 : timestamp.getAttribute('datetime');
834
+ if (!timestamp || !value) return;
835
+ var date = value instanceof Date ? value : new Date(value);
836
+ if (Number.isNaN(date.getTime())) return;
837
+ timestamp.setAttribute('datetime', date.toISOString());
838
+ timestamp.textContent = this.formatMessageTimestamp(date);
839
+ }
840
+ }, {
841
+ key: "formatMessageTimestamp",
842
+ value: function formatMessageTimestamp(date) {
843
+ return this.constructor.messageTimestampFormatterFor(Locale.toString()).format(date);
844
+ }
845
+ }, {
846
+ key: "messageFailureReason",
847
+ value: function () {
848
+ var _messageFailureReason = _asyncToGenerator(function* (response) {
849
+ var nativeResponse = (response === null || response === void 0 ? void 0 : response.data) || (response === null || response === void 0 ? void 0 : response.response);
850
+ var fallback = (nativeResponse === null || nativeResponse === void 0 ? void 0 : nativeResponse.statusText) || 'Message failed';
851
+ try {
852
+ var _jsonResponse$json;
853
+ var jsonResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
854
+ var payload = yield jsonResponse === null || jsonResponse === void 0 ? void 0 : (_jsonResponse$json = jsonResponse.json) === null || _jsonResponse$json === void 0 ? void 0 : _jsonResponse$json.call(jsonResponse);
855
+ var reason = this.messageFailureReasonFromPayload(payload);
856
+ if (reason) return reason;
857
+ } catch (_) {
858
+ // Fall through to text parsing/fallback. Some stores strip content-type or
859
+ // return non-JSON failures, so the timestamp still needs a useful reason.
860
+ }
861
+ try {
862
+ var _textResponse$text;
863
+ var textResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
864
+ var text = yield textResponse === null || textResponse === void 0 ? void 0 : (_textResponse$text = textResponse.text) === null || _textResponse$text === void 0 ? void 0 : _textResponse$text.call(textResponse);
865
+ return this.messageFailureReasonFromText(text) || fallback;
866
+ } catch (_) {
867
+ return fallback;
868
+ }
869
+ });
870
+ function messageFailureReason(_x6) {
871
+ return _messageFailureReason.apply(this, arguments);
872
+ }
873
+ return messageFailureReason;
874
+ }()
875
+ }, {
876
+ key: "messageFailureReasonFromText",
877
+ value: function messageFailureReasonFromText(text) {
878
+ if (typeof text !== 'string') return null;
879
+ var value = text.trim();
880
+ if (!value || value.startsWith('<')) return null;
881
+ try {
882
+ return this.messageFailureReasonFromPayload(JSON.parse(value)) || value;
883
+ } catch (_) {
884
+ return value;
885
+ }
886
+ }
887
+ }, {
888
+ key: "messageFailureReasonFromPayload",
889
+ value: function messageFailureReasonFromPayload(payload) {
890
+ var _payload$error, _payload$errors, _payload$errors2, _payload$errors2$, _payload$errors3, _payload$errors3$;
891
+ if (!payload) return null;
892
+ return [(_payload$error = payload.error) === null || _payload$error === void 0 ? void 0 : _payload$error.message, payload.message, (_payload$errors = payload.errors) === null || _payload$errors === void 0 ? void 0 : _payload$errors.message, (_payload$errors2 = payload.errors) === null || _payload$errors2 === void 0 ? void 0 : (_payload$errors2$ = _payload$errors2[0]) === null || _payload$errors2$ === void 0 ? void 0 : _payload$errors2$.message, (_payload$errors3 = payload.errors) === null || _payload$errors3 === void 0 ? void 0 : (_payload$errors3$ = _payload$errors3[0]) === null || _payload$errors3$ === void 0 ? void 0 : _payload$errors3$.description].find(reason => typeof reason === 'string' && reason.trim().length > 0);
893
+ }
705
894
  }, {
706
895
  key: "messageAttachmentsContainer",
707
896
  value: function messageAttachmentsContainer(element) {
@@ -832,9 +1021,28 @@ var _default = /*#__PURE__*/function (_Controller) {
832
1021
  get: function get() {
833
1022
  return window.matchMedia("(max-width: ".concat(this.fullScreenThresholdValue, "px)")).matches;
834
1023
  }
1024
+ }], [{
1025
+ key: "messageTimestampFormatterFor",
1026
+ value: function messageTimestampFormatterFor(locale) {
1027
+ var key = locale || 'default';
1028
+ if (!this.messageTimestampFormatters[key]) {
1029
+ this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale);
1030
+ }
1031
+ return this.messageTimestampFormatters[key];
1032
+ }
1033
+ }, {
1034
+ key: "buildMessageTimestampFormatter",
1035
+ value: function buildMessageTimestampFormatter(locale) {
1036
+ try {
1037
+ return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1038
+ } catch (_) {
1039
+ return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1040
+ }
1041
+ }
835
1042
  }]);
836
1043
  return _default;
837
1044
  }(Controller);
1045
+ _default.messageTimestampFormatters = {};
838
1046
  _default.values = {
839
1047
  id: String,
840
1048
  conversationId: String,
@@ -39,6 +39,7 @@ let Configuration = /*#__PURE__*/function () {
39
39
  */
40
40
  function assign(props) {
41
41
  if (props) {
42
+ const shouldInferActionCableUrl = Object.prototype.hasOwnProperty.call(props, 'apiRoot') && !Object.prototype.hasOwnProperty.call(props, 'actionCableUrl');
42
43
  Object.entries(props).forEach(([key, value]) => {
43
44
  if (key === 'forms') {
44
45
  this.forms = _forms.Forms.assign(value);
@@ -48,6 +49,9 @@ let Configuration = /*#__PURE__*/function () {
48
49
  this[key] = value;
49
50
  }
50
51
  });
52
+ if (shouldInferActionCableUrl) {
53
+ this.actionCableUrl = this.actionCableUrlForApiRoot(this.apiRoot);
54
+ }
51
55
  }
52
56
  return this;
53
57
  }
@@ -64,6 +68,21 @@ let Configuration = /*#__PURE__*/function () {
64
68
  value: function endpoint(path) {
65
69
  return `${this.apiRoot}/${path}`;
66
70
  }
71
+ }, {
72
+ key: "actionCableUrlForApiRoot",
73
+ value: function actionCableUrlForApiRoot(apiRoot) {
74
+ try {
75
+ const url = new URL(apiRoot);
76
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
77
+ url.protocol = protocol;
78
+ url.pathname = '/cable';
79
+ url.search = '';
80
+ url.hash = '';
81
+ return url.toString();
82
+ } catch (_) {
83
+ return this.actionCableUrl;
84
+ }
85
+ }
67
86
  }]);
68
87
  return Configuration;
69
88
  }();
@@ -34,6 +34,7 @@ var Configuration = /*#__PURE__*/function () {
34
34
  */
35
35
  function assign(props) {
36
36
  if (props) {
37
+ var shouldInferActionCableUrl = Object.prototype.hasOwnProperty.call(props, 'apiRoot') && !Object.prototype.hasOwnProperty.call(props, 'actionCableUrl');
37
38
  Object.entries(props).forEach(_ref => {
38
39
  var [key, value] = _ref;
39
40
  if (key === 'forms') {
@@ -44,6 +45,9 @@ var Configuration = /*#__PURE__*/function () {
44
45
  this[key] = value;
45
46
  }
46
47
  });
48
+ if (shouldInferActionCableUrl) {
49
+ this.actionCableUrl = this.actionCableUrlForApiRoot(this.apiRoot);
50
+ }
47
51
  }
48
52
  return this;
49
53
  }
@@ -60,6 +64,21 @@ var Configuration = /*#__PURE__*/function () {
60
64
  value: function endpoint(path) {
61
65
  return "".concat(this.apiRoot, "/").concat(path);
62
66
  }
67
+ }, {
68
+ key: "actionCableUrlForApiRoot",
69
+ value: function actionCableUrlForApiRoot(apiRoot) {
70
+ try {
71
+ var url = new URL(apiRoot);
72
+ var protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
73
+ url.protocol = protocol;
74
+ url.pathname = '/cable';
75
+ url.search = '';
76
+ url.hash = '';
77
+ return url.toString();
78
+ } catch (_) {
79
+ return this.actionCableUrl;
80
+ }
81
+ }
63
82
  }]);
64
83
  return Configuration;
65
84
  }();
@@ -12,17 +12,22 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
12
12
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
13
13
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
14
14
  function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
15
+ const stylesheetAttribute = 'data-hellotext-stylesheet';
16
+ const stylesheetLoadTimeout = 10000;
17
+
15
18
  /**
16
19
  * @typedef {Object} BusinessCountry
17
20
  * @property {String} [code] - ISO country code configured for the business.
18
21
  * @property {String} [prefix] - Phone country prefix configured for the business.
19
22
  */
23
+
20
24
  /**
21
25
  * @typedef {Object} BusinessWebchat
22
26
  * @property {String} [id] - Dashboard webchat id configured for the business.
23
27
  * @property {Object} [appearance] - Dashboard appearance defaults for the webchat.
24
28
  * @property {Object} [whatsapp] - Dashboard WhatsApp handoff defaults for the webchat.
25
29
  */
30
+
26
31
  /**
27
32
  * Public business metadata returned by `public/businesses/:id`.
28
33
  *
@@ -36,6 +41,7 @@ function _toPrimitive(input, hint) { if (typeof input !== "object" || input ===
36
41
  * @property {String|Array<String>} [whitelist] - Domain whitelist configuration.
37
42
  * @property {String} [subscription] - Current business subscription tier.
38
43
  */
44
+
39
45
  /**
40
46
  * Public business context used by the SDK for tracking, forms, and webchat defaults.
41
47
  */
@@ -47,6 +53,8 @@ let Business = /*#__PURE__*/function () {
47
53
  _classCallCheck(this, Business);
48
54
  this.id = id;
49
55
  this.data = null;
56
+ this.stylesheet = null;
57
+ this.stylesheetLoaded = Promise.resolve(false);
50
58
  }
51
59
 
52
60
  /**
@@ -87,11 +95,12 @@ let Business = /*#__PURE__*/function () {
87
95
  key: "setData",
88
96
  value: function setData(data) {
89
97
  this.data = data;
90
- if (typeof document !== 'undefined') {
91
- const linkTag = document.createElement('link');
92
- linkTag.rel = 'stylesheet';
93
- linkTag.href = data.style_url;
94
- document.head.append(linkTag);
98
+ if (typeof document !== 'undefined' && data.style_url) {
99
+ this.stylesheet = this.constructor.ensureStylesheet(data.style_url);
100
+ this.stylesheetLoaded = this.constructor.waitForStylesheet(this.stylesheet);
101
+ } else {
102
+ this.stylesheet = null;
103
+ this.stylesheetLoaded = Promise.resolve(false);
95
104
  }
96
105
  }
97
106
  }, {
@@ -135,6 +144,78 @@ let Business = /*#__PURE__*/function () {
135
144
  get: function () {
136
145
  return this.data.features;
137
146
  }
147
+ }], [{
148
+ key: "stylesheetSelector",
149
+ get: function () {
150
+ return `link[rel="stylesheet"][${stylesheetAttribute}]`;
151
+ }
152
+ }, {
153
+ key: "ensureStylesheet",
154
+ value: function ensureStylesheet(styleUrl) {
155
+ const href = this.normalizedStylesheetUrl(styleUrl);
156
+ const existingLink = this.stylesheetLinks.find(link => link.href === href);
157
+ if (existingLink) {
158
+ existingLink.setAttribute(stylesheetAttribute, 'true');
159
+ return existingLink;
160
+ }
161
+ const linkTag = document.createElement('link');
162
+ linkTag.rel = 'stylesheet';
163
+ linkTag.href = styleUrl;
164
+ linkTag.setAttribute(stylesheetAttribute, 'true');
165
+ this.waitForStylesheet(linkTag);
166
+ document.head.append(linkTag);
167
+ return linkTag;
168
+ }
169
+ }, {
170
+ key: "stylesheetLinks",
171
+ get: function () {
172
+ if (typeof document === 'undefined') return [];
173
+ return Array.from(document.querySelectorAll(this.stylesheetSelector));
174
+ }
175
+ }, {
176
+ key: "latestStylesheet",
177
+ get: function () {
178
+ return this.stylesheetLinks[this.stylesheetLinks.length - 1];
179
+ }
180
+ }, {
181
+ key: "normalizedStylesheetUrl",
182
+ value: function normalizedStylesheetUrl(styleUrl) {
183
+ try {
184
+ return new URL(styleUrl, document.baseURI).href;
185
+ } catch (_error) {
186
+ return styleUrl;
187
+ }
188
+ }
189
+ }, {
190
+ key: "waitForStylesheet",
191
+ value: function waitForStylesheet(linkTag) {
192
+ if (!linkTag) return Promise.resolve(false);
193
+ if (this.stylesheetIsLoaded(linkTag)) return Promise.resolve(true);
194
+ if (linkTag.dataset.hellotextStylesheetLoaded === 'false') return Promise.resolve(false);
195
+ if (linkTag._hellotextStylesheetLoaded) return linkTag._hellotextStylesheetLoaded;
196
+ linkTag._hellotextStylesheetLoaded = new Promise(resolve => {
197
+ let timeout;
198
+ const finish = loaded => {
199
+ clearTimeout(timeout);
200
+ linkTag.removeEventListener('load', handleLoad);
201
+ linkTag.removeEventListener('error', handleError);
202
+ linkTag.dataset.hellotextStylesheetLoaded = loaded ? 'true' : 'false';
203
+ resolve(loaded);
204
+ };
205
+ const handleLoad = () => finish(this.stylesheetIsLoaded(linkTag));
206
+ const handleError = () => finish(false);
207
+ linkTag.addEventListener('load', handleLoad);
208
+ linkTag.addEventListener('error', handleError);
209
+ timeout = setTimeout(() => finish(this.stylesheetIsLoaded(linkTag)), stylesheetLoadTimeout);
210
+ if (timeout.unref) timeout.unref();
211
+ });
212
+ return linkTag._hellotextStylesheetLoaded;
213
+ }
214
+ }, {
215
+ key: "stylesheetIsLoaded",
216
+ value: function stylesheetIsLoaded(linkTag) {
217
+ return linkTag.dataset.hellotextStylesheetLoaded === 'true' || !!linkTag.sheet;
218
+ }
138
219
  }]);
139
220
  return Business;
140
221
  }();