@hellotext/hellotext 2.5.6 → 2.5.8

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 (113) hide show
  1. package/README.md +5 -26
  2. package/dist/hellotext.js +1 -1
  3. package/dist/hellotext.js.LICENSE.txt +1 -1
  4. package/index.d.ts +15 -6
  5. package/lib/api/acks.cjs +17 -31
  6. package/lib/api/acks.js +17 -42
  7. package/lib/api/businesses.cjs +14 -28
  8. package/lib/api/businesses.js +13 -36
  9. package/lib/api/events.cjs +26 -40
  10. package/lib/api/events.js +28 -52
  11. package/lib/api/forms.cjs +24 -40
  12. package/lib/api/forms.js +24 -57
  13. package/lib/api/identifications.cjs +15 -29
  14. package/lib/api/identifications.js +15 -40
  15. package/lib/api/index.cjs +26 -46
  16. package/lib/api/index.js +28 -49
  17. package/lib/api/push/identities.cjs +43 -0
  18. package/lib/api/push/identities.js +35 -0
  19. package/lib/api/response.cjs +30 -52
  20. package/lib/api/response.js +31 -60
  21. package/lib/api/submissions.cjs +18 -34
  22. package/lib/api/submissions.js +18 -48
  23. package/lib/api/webchat/messages.cjs +44 -64
  24. package/lib/api/webchat/messages.js +44 -80
  25. package/lib/api/webchats.cjs +37 -56
  26. package/lib/api/webchats.js +37 -65
  27. package/lib/api/whatsapp_widgets.cjs +49 -72
  28. package/lib/api/whatsapp_widgets.js +48 -91
  29. package/lib/builders/input_builder.cjs +36 -48
  30. package/lib/builders/input_builder.js +36 -48
  31. package/lib/builders/logo_builder.cjs +10 -29
  32. package/lib/builders/logo_builder.js +20 -28
  33. package/lib/channels/application_channel.cjs +136 -178
  34. package/lib/channels/application_channel.js +137 -180
  35. package/lib/channels/webchat_channel.cjs +125 -170
  36. package/lib/channels/webchat_channel.js +125 -170
  37. package/lib/controllers/form_controller.cjs +82 -121
  38. package/lib/controllers/form_controller.js +82 -130
  39. package/lib/controllers/message_controller.cjs +211 -282
  40. package/lib/controllers/message_controller.js +209 -284
  41. package/lib/controllers/mixins/usePopover.cjs +3 -6
  42. package/lib/controllers/mixins/usePopover.js +17 -22
  43. package/lib/controllers/webchat/emoji_picker_controller.cjs +106 -150
  44. package/lib/controllers/webchat/emoji_picker_controller.js +105 -170
  45. package/lib/controllers/webchat/useBehaviour.js +5 -5
  46. package/lib/controllers/webchat/useOpeningSequence.js +6 -6
  47. package/lib/controllers/webchat/useTeaser.js +9 -9
  48. package/lib/controllers/webchat_controller.cjs +916 -1088
  49. package/lib/controllers/webchat_controller.js +926 -1152
  50. package/lib/core/configuration/forms.cjs +21 -35
  51. package/lib/core/configuration/forms.js +20 -35
  52. package/lib/core/configuration/locale.cjs +36 -71
  53. package/lib/core/configuration/locale.js +36 -70
  54. package/lib/core/configuration/push.cjs +16 -0
  55. package/lib/core/configuration/push.js +10 -0
  56. package/lib/core/configuration/webchat.cjs +142 -182
  57. package/lib/core/configuration/webchat.js +145 -189
  58. package/lib/core/configuration/whatsapp.cjs +76 -102
  59. package/lib/core/configuration/whatsapp.js +76 -105
  60. package/lib/core/configuration.cjs +62 -75
  61. package/lib/core/configuration.js +61 -75
  62. package/lib/core/event.cjs +36 -59
  63. package/lib/core/event.js +35 -61
  64. package/lib/core/sanitize_html.js +4 -4
  65. package/lib/errors/invalid_event.cjs +5 -26
  66. package/lib/errors/invalid_event.js +5 -26
  67. package/lib/errors/not_initialized_error.cjs +5 -26
  68. package/lib/errors/not_initialized_error.js +5 -26
  69. package/lib/hellotext.cjs +186 -211
  70. package/lib/hellotext.js +189 -236
  71. package/lib/index.js +1 -1
  72. package/lib/models/business.cjs +115 -152
  73. package/lib/models/business.js +118 -162
  74. package/lib/models/cookies.cjs +34 -51
  75. package/lib/models/cookies.js +34 -51
  76. package/lib/models/fingerprint.cjs +8 -23
  77. package/lib/models/fingerprint.js +25 -58
  78. package/lib/models/form.cjs +106 -142
  79. package/lib/models/form.js +105 -153
  80. package/lib/models/form_collection.cjs +63 -101
  81. package/lib/models/form_collection.js +61 -107
  82. package/lib/models/index.cjs +7 -0
  83. package/lib/models/index.js +1 -0
  84. package/lib/models/page.cjs +106 -129
  85. package/lib/models/page.js +106 -131
  86. package/lib/models/push.cjs +345 -0
  87. package/lib/models/push.js +339 -0
  88. package/lib/models/query.cjs +21 -41
  89. package/lib/models/query.js +21 -41
  90. package/lib/models/session.cjs +33 -65
  91. package/lib/models/session.js +30 -63
  92. package/lib/models/user.cjs +29 -51
  93. package/lib/models/user.js +29 -51
  94. package/lib/models/utm.cjs +15 -27
  95. package/lib/models/utm.js +17 -32
  96. package/lib/models/webchat.cjs +52 -76
  97. package/lib/models/webchat.js +52 -90
  98. package/lib/models/whatsapp_widget.cjs +41 -59
  99. package/lib/models/whatsapp_widget.js +41 -73
  100. package/lib/vanilla.cjs +4 -7
  101. package/lib/vanilla.js +2 -1
  102. package/package.json +2 -10
  103. package/src/api/index.js +5 -0
  104. package/src/api/push/identities.js +42 -0
  105. package/src/core/configuration/push.js +12 -0
  106. package/src/core/configuration.js +5 -0
  107. package/src/hellotext.js +15 -1
  108. package/src/index.bundle.js +0 -1
  109. package/src/models/business.js +1 -0
  110. package/src/models/index.js +1 -0
  111. package/src/models/push.js +397 -0
  112. package/src/vanilla.js +2 -1
  113. package/styles/index.css +0 -30
@@ -1,22 +1,3 @@
1
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
2
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
3
- function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
5
- function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
6
- function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
7
- function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
8
- function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
9
- function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
10
- 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); }
11
- function _get() { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get.bind(); } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(arguments.length < 3 ? target : receiver); } return desc.value; }; } return _get.apply(this, arguments); }
12
- function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
13
- function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
14
- function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
15
- function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
16
- function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
17
- function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
18
- function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
19
- function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
20
1
  import { flip, offset, shift } from '@floating-ui/dom';
21
2
  import { Controller } from '@hotwired/stimulus';
22
3
  import WebchatMessagesAPI from '../api/webchat/messages';
@@ -29,1198 +10,991 @@ import { usePopover } from './mixins/usePopover';
29
10
  import { useBehaviour } from './webchat/useBehaviour';
30
11
  import { useOpeningSequence } from './webchat/useOpeningSequence';
31
12
  import { useTeaser } from './webchat/useTeaser';
32
- var POPOVER_ANIMATION_DURATION = 120;
33
- var MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
13
+ const POPOVER_ANIMATION_DURATION = 120;
14
+ const MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
34
15
  hour: 'numeric',
35
16
  minute: '2-digit'
36
17
  };
37
- var MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
38
- var SCROLL_ISOLATION_EVENT_OPTIONS = {
18
+ const MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
19
+ const SCROLL_ISOLATION_EVENT_OPTIONS = {
39
20
  capture: true,
40
21
  passive: true
41
22
  };
42
- var _default = /*#__PURE__*/function (_Controller) {
43
- _inherits(_default, _Controller);
44
- var _super = _createSuper(_default);
45
- function _default() {
46
- _classCallCheck(this, _default);
47
- return _super.apply(this, arguments);
48
- }
49
- _createClass(_default, [{
50
- key: "initialize",
51
- value: function initialize() {
52
- this.messagesAPI = new WebchatMessagesAPI(this.idValue);
53
- this.webChatChannel = new WebchatChannel(this.idValue, Hellotext.session, this.conversationIdValue);
54
- this.files = [];
55
- this.messageIds = new Set();
56
- this.catchUpAfterMessageId = null;
57
- this.fetchingCatchUpMessages = false;
58
- this.onMessageReceived = this.onMessageReceived.bind(this);
59
- this.onMessageReaction = this.onMessageReaction.bind(this);
60
- this.onTypingStart = this.onTypingStart.bind(this);
61
- this.captureCatchUpCursor = this.captureCatchUpCursor.bind(this);
62
- this.catchUpMessages = this.catchUpMessages.bind(this);
63
- this.onScroll = this.onScroll.bind(this);
64
- this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
65
- this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
66
- this.broadcastChannel = new BroadcastChannel("hellotext--webchat--".concat(this.idValue));
67
- this.webChatChannel.onDisconnect(this.captureCatchUpCursor);
68
- this.webChatChannel.onReconnect(this.catchUpMessages);
69
- _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
70
- }
71
- }, {
72
- key: "connect",
73
- value: function connect() {
74
- useBehaviour(this);
75
- usePopover(this);
76
- useOpeningSequence(this);
77
- useTeaser(this);
23
+ export default class extends Controller {
24
+ static messageTimestampFormatters = {};
25
+ static values = {
26
+ id: String,
27
+ conversationId: String,
28
+ media: Object,
29
+ fileSizeErrorMessage: String,
30
+ placement: {
31
+ type: String,
32
+ default: 'bottom-end'
33
+ },
34
+ open: {
35
+ type: Boolean,
36
+ default: false
37
+ },
38
+ autoPlacement: {
39
+ type: Boolean,
40
+ default: false
41
+ },
42
+ disabled: {
43
+ type: Boolean,
44
+ default: false
45
+ },
46
+ nextPage: {
47
+ type: Number,
48
+ default: undefined
49
+ },
50
+ fullScreenThreshold: {
51
+ type: Number,
52
+ default: 1024
53
+ },
54
+ typingIndicatorKeepAlive: {
55
+ type: Number,
56
+ default: 30000
57
+ },
58
+ // 30 seconds
59
+ offset: {
60
+ type: Number,
61
+ default: 24
62
+ },
63
+ padding: {
64
+ type: Number,
65
+ default: 24
66
+ },
67
+ optimisticTypingIndicatorWait: {
68
+ type: Number,
69
+ default: 1000
70
+ },
71
+ // 1 second,
72
+ teaser: Object,
73
+ messageTeaser: String,
74
+ behaviour: Object
75
+ };
76
+ static classes = ['fadeOut'];
77
+ static targets = ['trigger', 'popover', 'input', 'attachmentInput', 'attachmentButton', 'errorMessageContainer', 'attachmentTemplate', 'attachmentContainer', 'attachment', 'messageTemplate', 'messagesContainer', 'title', 'attachmentImage', 'footer', 'toolbar', 'message', 'unreadCounter', 'typingIndicator', 'typingIndicatorTemplate', 'teaser', 'teaserMessage', 'inboundMessageTeaser', 'inboundMessageTeaserBody', 'openingSequence', 'openingSequenceMessage'];
78
+ initialize() {
79
+ this.messagesAPI = new WebchatMessagesAPI(this.idValue);
80
+ this.webChatChannel = new WebchatChannel(this.idValue, Hellotext.session, this.conversationIdValue);
81
+ this.files = [];
82
+ this.messageIds = new Set();
83
+ this.catchUpAfterMessageId = null;
84
+ this.fetchingCatchUpMessages = false;
85
+ this.onMessageReceived = this.onMessageReceived.bind(this);
86
+ this.onMessageReaction = this.onMessageReaction.bind(this);
87
+ this.onTypingStart = this.onTypingStart.bind(this);
88
+ this.captureCatchUpCursor = this.captureCatchUpCursor.bind(this);
89
+ this.catchUpMessages = this.catchUpMessages.bind(this);
90
+ this.onScroll = this.onScroll.bind(this);
91
+ this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
92
+ this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
93
+ this.broadcastChannel = new BroadcastChannel(`hellotext--webchat--${this.idValue}`);
94
+ this.webChatChannel.onDisconnect(this.captureCatchUpCursor);
95
+ this.webChatChannel.onReconnect(this.catchUpMessages);
96
+ super.initialize();
97
+ }
98
+ connect() {
99
+ useBehaviour(this);
100
+ usePopover(this);
101
+ useOpeningSequence(this);
102
+ useTeaser(this);
103
+ this.setupFloatingUI({
104
+ trigger: this.triggerTarget,
105
+ popover: this.popoverTarget
106
+ });
107
+ if (this.hasTeaserTarget) {
78
108
  this.setupFloatingUI({
79
109
  trigger: this.triggerTarget,
80
- popover: this.popoverTarget
110
+ popover: this.teaserTarget,
111
+ strategy: 'absolute'
81
112
  });
82
- if (this.hasTeaserTarget) {
83
- this.setupFloatingUI({
84
- trigger: this.triggerTarget,
85
- popover: this.teaserTarget,
86
- strategy: 'absolute'
87
- });
88
- }
89
- this.setupTeaser();
90
- this.setupOpeningSequence();
91
- this.localizeMessageTimestamps();
92
- this.webChatChannel.onMessage(this.onMessageReceived);
93
- this.webChatChannel.onTypingStart(this.onTypingStart);
94
- this.webChatChannel.onReaction(this.onMessageReaction);
95
- this.setupMessagesContainerScrollIsolation();
96
- this.messagesContainerTarget.addEventListener('scroll', this.onScroll);
97
- this.messagesContainerTarget.addEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
98
- this.messagesContainerTarget.addEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
99
- if (this.shouldOpenOnMount) {
100
- this.openValue = true;
101
- }
102
- Hellotext.eventEmitter.dispatch('webchat:mounted');
103
- this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent);
104
- window.addEventListener('keydown', this.closePopoverOnEscape, true);
105
- this.scheduleBehaviourOpen();
106
- _get(_getPrototypeOf(_default.prototype), "connect", this).call(this);
107
113
  }
108
- }, {
109
- key: "disconnect",
110
- value: function disconnect() {
111
- this.cancelBehaviourOpen();
112
- this.clearPopoverOpenAnimation();
113
- this.teardownTeaser();
114
- this.teardownOpeningSequence();
115
- this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
116
- this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
117
- this.messagesContainerTarget.removeEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
118
- this.messagesContainerTarget.removeEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
119
- window.removeEventListener('keydown', this.closePopoverOnEscape, true);
114
+ this.setupTeaser();
115
+ this.setupOpeningSequence();
116
+ this.localizeMessageTimestamps();
117
+ this.webChatChannel.onMessage(this.onMessageReceived);
118
+ this.webChatChannel.onTypingStart(this.onTypingStart);
119
+ this.webChatChannel.onReaction(this.onMessageReaction);
120
+ this.setupMessagesContainerScrollIsolation();
121
+ this.messagesContainerTarget.addEventListener('scroll', this.onScroll);
122
+ this.messagesContainerTarget.addEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
123
+ this.messagesContainerTarget.addEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
124
+ if (this.shouldOpenOnMount) {
125
+ this.openValue = true;
126
+ }
127
+ Hellotext.eventEmitter.dispatch('webchat:mounted');
128
+ this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent);
129
+ window.addEventListener('keydown', this.closePopoverOnEscape, true);
130
+ this.scheduleBehaviourOpen();
131
+ super.connect();
132
+ }
133
+ disconnect() {
134
+ this.cancelBehaviourOpen();
135
+ this.clearPopoverOpenAnimation();
136
+ this.teardownTeaser();
137
+ this.teardownOpeningSequence();
138
+ this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
139
+ this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
140
+ this.messagesContainerTarget.removeEventListener('wheel', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
141
+ this.messagesContainerTarget.removeEventListener('touchmove', this.stopHostScrollPropagation, SCROLL_ISOLATION_EVENT_OPTIONS);
142
+ window.removeEventListener('keydown', this.closePopoverOnEscape, true);
120
143
 
121
- // Clean up typing indicator timeouts
122
- this.clearTypingIndicator();
123
- this.broadcastChannel.close();
124
- this.floatingUICleanup();
125
- _get(_getPrototypeOf(_default.prototype), "disconnect", this).call(this);
126
- }
127
- }, {
128
- key: "setupMessagesContainerScrollIsolation",
129
- value: function setupMessagesContainerScrollIsolation() {
130
- this.messagesContainerTarget.style.overscrollBehavior = 'contain';
131
- this.messagesContainerTarget.style.webkitOverflowScrolling = 'touch';
132
- this.messagesContainerTarget.style.touchAction = 'pan-y';
133
- this.messagesContainerTarget.setAttribute('data-lenis-prevent', '');
134
- this.messagesContainerTarget.setAttribute('data-lenis-prevent-wheel', '');
135
- this.messagesContainerTarget.setAttribute('data-lenis-prevent-touch', '');
136
- }
137
- }, {
138
- key: "stopHostScrollPropagation",
139
- value: function stopHostScrollPropagation(event) {
140
- event.stopPropagation();
141
- }
142
- }, {
143
- key: "onTypingStart",
144
- value: function onTypingStart() {
145
- if (this.typingIndicatorVisible) {
146
- return this.resetTypingIndicatorTimer();
147
- }
148
- this.showTypingIndicator();
149
- }
150
- }, {
151
- key: "showOptimisticTypingIndicator",
152
- value: function showOptimisticTypingIndicator() {
153
- if (this.typingIndicatorVisible) return;
154
- this.showTypingIndicator();
144
+ // Clean up typing indicator timeouts
145
+ this.clearTypingIndicator();
146
+ this.broadcastChannel.close();
147
+ this.floatingUICleanup();
148
+ super.disconnect();
149
+ }
150
+ setupMessagesContainerScrollIsolation() {
151
+ this.messagesContainerTarget.style.overscrollBehavior = 'contain';
152
+ this.messagesContainerTarget.style.webkitOverflowScrolling = 'touch';
153
+ this.messagesContainerTarget.style.touchAction = 'pan-y';
154
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent', '');
155
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-wheel', '');
156
+ this.messagesContainerTarget.setAttribute('data-lenis-prevent-touch', '');
157
+ }
158
+ stopHostScrollPropagation(event) {
159
+ event.stopPropagation();
160
+ }
161
+ onTypingStart() {
162
+ if (this.typingIndicatorVisible) {
163
+ return this.resetTypingIndicatorTimer();
155
164
  }
156
- }, {
157
- key: "showTypingIndicator",
158
- value: function showTypingIndicator() {
159
- this.clearTypingIndicator();
160
- this.typingIndicatorVisible = true;
161
- var indicator = this.typingIndicatorTemplateTarget.cloneNode(true);
162
- indicator.setAttribute('data-hellotext--webchat-target', 'typingIndicator');
163
- indicator.style.display = 'flex';
164
- this.messagesContainerTarget.appendChild(indicator);
165
- requestAnimationFrame(() => {
166
- this.messagesContainerTarget.scroll({
167
- top: this.messagesContainerTarget.scrollHeight,
168
- behavior: 'instant'
169
- });
165
+ this.showTypingIndicator();
166
+ }
167
+ showOptimisticTypingIndicator() {
168
+ if (this.typingIndicatorVisible) return;
169
+ this.showTypingIndicator();
170
+ }
171
+ showTypingIndicator() {
172
+ this.clearTypingIndicator();
173
+ this.typingIndicatorVisible = true;
174
+ const indicator = this.typingIndicatorTemplateTarget.cloneNode(true);
175
+ indicator.setAttribute('data-hellotext--webchat-target', 'typingIndicator');
176
+ indicator.style.display = 'flex';
177
+ this.messagesContainerTarget.appendChild(indicator);
178
+ requestAnimationFrame(() => {
179
+ this.messagesContainerTarget.scroll({
180
+ top: this.messagesContainerTarget.scrollHeight,
181
+ behavior: 'instant'
170
182
  });
171
- var timeout = this.typingIndicatorKeepAliveValue;
172
- this.incomingTypingIndicatorTimeout = setTimeout(() => {
173
- this.clearTypingIndicator();
174
- }, timeout);
175
- }
176
- }, {
177
- key: "resetTypingIndicatorTimer",
178
- value: function resetTypingIndicatorTimer() {
179
- if (!this.typingIndicatorVisible) return;
183
+ });
184
+ const timeout = this.typingIndicatorKeepAliveValue;
185
+ this.incomingTypingIndicatorTimeout = setTimeout(() => {
186
+ this.clearTypingIndicator();
187
+ }, timeout);
188
+ }
189
+ resetTypingIndicatorTimer() {
190
+ if (!this.typingIndicatorVisible) return;
180
191
 
181
- // Clear existing timeout
182
- clearTimeout(this.incomingTypingIndicatorTimeout);
183
- clearTimeout(this.optimisticTypingTimeout);
192
+ // Clear existing timeout
193
+ clearTimeout(this.incomingTypingIndicatorTimeout);
194
+ clearTimeout(this.optimisticTypingTimeout);
184
195
 
185
- // Use unified timeout for all typing indicators
186
- var timeout = this.typingIndicatorKeepAliveValue;
187
- this.incomingTypingIndicatorTimeout = setTimeout(() => {
188
- this.clearTypingIndicator();
189
- }, timeout);
190
- }
191
- }, {
192
- key: "clearTypingIndicator",
193
- value: function clearTypingIndicator() {
194
- if (this.hasTypingIndicatorTarget) {
195
- this.typingIndicatorTarget.remove();
196
- }
197
- this.typingIndicatorVisible = false;
198
- clearTimeout(this.incomingTypingIndicatorTimeout);
199
- clearTimeout(this.optimisticTypingTimeout);
200
- }
201
- }, {
202
- key: "onMessageInputChange",
203
- value: function onMessageInputChange() {
204
- this.resizeInput();
205
- clearTimeout(this.typingIndicatorTimeout);
206
- if (!this.hasSentTypingIndicator) {
207
- this.webChatChannel.startTypingIndicator();
208
- this.hasSentTypingIndicator = true;
209
- }
210
- this.typingIndicatorTimeout = setTimeout(() => {
211
- this.hasSentTypingIndicator = false;
212
- }, 3000);
196
+ // Use unified timeout for all typing indicators
197
+ const timeout = this.typingIndicatorKeepAliveValue;
198
+ this.incomingTypingIndicatorTimeout = setTimeout(() => {
199
+ this.clearTypingIndicator();
200
+ }, timeout);
201
+ }
202
+ clearTypingIndicator() {
203
+ if (this.hasTypingIndicatorTarget) {
204
+ this.typingIndicatorTarget.remove();
213
205
  }
214
- }, {
215
- key: "onOutboundMessageSent",
216
- value: function onOutboundMessageSent(event) {
217
- var {
218
- data
219
- } = event;
220
- var callbacks = {
221
- 'message:sent': data => {
222
- var element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
223
- this.localizeMessageTimestamps(element);
206
+ this.typingIndicatorVisible = false;
207
+ clearTimeout(this.incomingTypingIndicatorTimeout);
208
+ clearTimeout(this.optimisticTypingTimeout);
209
+ }
210
+ onMessageInputChange() {
211
+ this.resizeInput();
212
+ clearTimeout(this.typingIndicatorTimeout);
213
+ if (!this.hasSentTypingIndicator) {
214
+ this.webChatChannel.startTypingIndicator();
215
+ this.hasSentTypingIndicator = true;
216
+ }
217
+ this.typingIndicatorTimeout = setTimeout(() => {
218
+ this.hasSentTypingIndicator = false;
219
+ }, 3000);
220
+ }
221
+ onOutboundMessageSent(event) {
222
+ const {
223
+ data
224
+ } = event;
225
+ const callbacks = {
226
+ 'message:sent': data => {
227
+ const element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
228
+ this.localizeMessageTimestamps(element);
224
229
 
225
- // Insert message before typing indicator if one exists
226
- if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
227
- this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
228
- } else {
229
- this.messagesContainerTarget.appendChild(element);
230
- }
231
- element.scrollIntoView({
232
- behavior: 'instant'
233
- });
234
- },
235
- 'message:failed': data => {
236
- var element = this.messagesContainerTarget.querySelector("#".concat(data.id));
237
- this.markMessageFailed(element, data.reason);
230
+ // Insert message before typing indicator if one exists
231
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
232
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
233
+ } else {
234
+ this.messagesContainerTarget.appendChild(element);
238
235
  }
239
- };
240
- if (callbacks[data.type]) {
241
- callbacks[data.type](data);
242
- } else {
243
- console.log("Unhandled message event: ".concat(data.type));
244
- }
245
- }
246
- }, {
247
- key: "onScroll",
248
- value: function () {
249
- var _onScroll = _asyncToGenerator(function* () {
250
- if (this.messagesContainerTarget.scrollTop > 300 || !this.nextPageValue || this.fetchingNextPage) return;
251
- this.fetchingNextPage = true;
252
- var response = yield this.messagesAPI.index({
253
- page: this.nextPageValue,
254
- session: Hellotext.session
255
- });
256
- var {
257
- next: nextPage,
258
- messages
259
- } = yield response.json();
260
- this.nextPageValue = nextPage;
261
- this.oldScrollHeight = this.messagesContainerTarget.scrollHeight;
262
- messages.forEach(message => {
263
- var {
264
- body,
265
- attachments
266
- } = message;
267
- var createdAt = message.created_at || message.createdAt;
268
- var element = this.messageTemplateTarget.cloneNode(true);
269
- element.classList.add('hellotext--webchat-message');
270
- element.setAttribute('data-hellotext--webchat-target', 'message');
271
- element.setAttribute('data-id', message.id);
272
- if (createdAt) {
273
- element.setAttribute('data-created-at', createdAt);
274
- }
275
- element.style.removeProperty('display');
276
- setSanitizedRichText(element.querySelector('[data-body]'), body);
277
- if (message.state === 'received') {
278
- element.classList.add('received');
279
- } else {
280
- element.classList.remove('received');
281
- }
282
- if (attachments) {
283
- attachments.forEach(attachmentUrl => {
284
- var _this$messageAttachme;
285
- var image = this.attachmentImageTarget.cloneNode(true);
286
- image.removeAttribute('data-hellotext--webchat-target');
287
- image.src = attachmentUrl;
288
- image.style.display = 'block';
289
- (_this$messageAttachme = this.messageAttachmentsContainer(element)) === null || _this$messageAttachme === void 0 ? void 0 : _this$messageAttachme.appendChild(image);
290
- });
291
- }
292
- element.setAttribute('data-body', body);
293
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
294
- this.messagesContainerTarget.prepend(element);
295
- });
296
- this.messagesContainerTarget.scroll({
297
- top: this.messagesContainerTarget.scrollHeight - this.oldScrollHeight,
236
+ element.scrollIntoView({
298
237
  behavior: 'instant'
299
238
  });
300
- this.fetchingNextPage = false;
301
- });
302
- function onScroll() {
303
- return _onScroll.apply(this, arguments);
304
- }
305
- return onScroll;
306
- }()
307
- }, {
308
- key: "onClickOutside",
309
- value: function onClickOutside(event) {
310
- if (WebchatConfiguration.mode === modes.POPOVER && this.openValue && event.target.nodeType && this.element.contains(event.target) === false) {
311
- this.openValue = false;
312
- }
313
- }
314
- }, {
315
- key: "closePopover",
316
- value: function closePopover() {
317
- this.clearPopoverOpenAnimation();
318
- this.popoverTarget.classList.remove(...this.fadeOutClasses);
319
- this.openValue = false;
320
- }
321
- }, {
322
- key: "preparePopoverOpenAnimation",
323
- value: function preparePopoverOpenAnimation() {
324
- this.clearPopoverOpenAnimation();
325
- this.popoverTarget.classList.remove(...this.fadeOutClasses);
326
- this.popoverTarget.classList.add('hellotext--webchat-popover-opening');
327
- this.popoverOpenAnimationTimeout = setTimeout(() => {
328
- this.popoverTarget.classList.remove('hellotext--webchat-popover-opening');
329
- this.popoverOpenAnimationTimeout = null;
330
- }, POPOVER_ANIMATION_DURATION);
331
- }
332
- }, {
333
- key: "clearPopoverOpenAnimation",
334
- value: function clearPopoverOpenAnimation() {
335
- var _this$popoverTarget;
336
- if (this.popoverOpenAnimationTimeout) {
337
- clearTimeout(this.popoverOpenAnimationTimeout);
338
- this.popoverOpenAnimationTimeout = null;
339
- }
340
- (_this$popoverTarget = this.popoverTarget) === null || _this$popoverTarget === void 0 ? void 0 : _this$popoverTarget.classList.remove('hellotext--webchat-popover-opening');
341
- }
342
- }, {
343
- key: "onPopoverOpened",
344
- value: function onPopoverOpened() {
345
- var _this$dismissTeaserFo;
346
- this.popoverTarget.classList.remove(...this.fadeOutClasses);
347
- (_this$dismissTeaserFo = this.dismissTeaserForSession) === null || _this$dismissTeaserFo === void 0 ? void 0 : _this$dismissTeaserFo.call(this);
348
- if (!this.onMobile) {
349
- this.focusComposeInput();
350
- }
351
- if (!this.scrolled) {
352
- requestAnimationFrame(() => {
353
- this.messagesContainerTarget.scroll({
354
- top: this.messagesContainerTarget.scrollHeight,
355
- behavior: 'instant'
356
- });
357
- });
358
- this.scrolled = true;
359
- }
360
- Hellotext.eventEmitter.dispatch('webchat:opened');
361
- localStorage.setItem("hellotext--webchat--".concat(this.idValue), 'opened');
362
- if (this.messageTeaserValue) {
363
- this.messageTeaserValue = null;
239
+ },
240
+ 'message:failed': data => {
241
+ const element = this.messagesContainerTarget.querySelector(`#${data.id}`);
242
+ this.markMessageFailed(element, data.reason);
364
243
  }
365
- this.startOpeningSequence();
366
- if (this.unreadCounterTarget.style.display === 'none') return;
367
- this.unreadCounterTarget.style.display = 'none';
368
- this.unreadCounterTarget.innerText = '0';
369
- this.messagesAPI.markAsSeen();
244
+ };
245
+ if (callbacks[data.type]) {
246
+ callbacks[data.type](data);
247
+ } else {
248
+ console.log(`Unhandled message event: ${data.type}`);
370
249
  }
371
- }, {
372
- key: "onPopoverClosed",
373
- value: function onPopoverClosed() {
374
- this.clearPopoverOpenAnimation();
375
- Hellotext.eventEmitter.dispatch('webchat:closed');
376
- localStorage.setItem("hellotext--webchat--".concat(this.idValue), 'closed');
377
- }
378
- }, {
379
- key: "onMessageReaction",
380
- value: function onMessageReaction(message) {
381
- var {
382
- message: messageId,
383
- reaction,
384
- type
385
- } = message;
386
- var messageElement = this.messageTargets.find(element => element.dataset.id === messageId);
387
- var reactionsContainer = messageElement.querySelector('[data-reactions]');
388
- if (type === 'reaction.destroy') {
389
- var reactionElement = reactionsContainer.querySelector("[data-id=\"".concat(reaction.id, "\"]"));
390
- return reactionElement.remove();
391
- }
392
- if (reactionsContainer.querySelector("[data-id=\"".concat(reaction.id, "\"]"))) {
393
- var _reactionElement = reactionsContainer.querySelector("[data-id=\"".concat(reaction.id, "\"]"));
394
- _reactionElement.innerText = reaction.emoji;
395
- } else {
396
- var _reactionElement2 = document.createElement('span');
397
- _reactionElement2.innerText = reaction.emoji;
398
- _reactionElement2.setAttribute('data-id', reaction.id);
399
- reactionsContainer.appendChild(_reactionElement2);
400
- }
401
- }
402
- }, {
403
- key: "onMessageReceived",
404
- value: function onMessageReceived(message) {
405
- var _this$hideTeaser;
406
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
407
- var {
408
- id,
250
+ }
251
+ async onScroll() {
252
+ if (this.messagesContainerTarget.scrollTop > 300 || !this.nextPageValue || this.fetchingNextPage) return;
253
+ this.fetchingNextPage = true;
254
+ const response = await this.messagesAPI.index({
255
+ page: this.nextPageValue,
256
+ session: Hellotext.session
257
+ });
258
+ const {
259
+ next: nextPage,
260
+ messages
261
+ } = await response.json();
262
+ this.nextPageValue = nextPage;
263
+ this.oldScrollHeight = this.messagesContainerTarget.scrollHeight;
264
+ messages.forEach(message => {
265
+ const {
409
266
  body,
410
- attachments,
411
- teaser
267
+ attachments
412
268
  } = message;
413
- var createdAt = message.created_at || message.createdAt;
414
- if (!this.claimMessageId(id)) return;
415
- (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
416
- if (message.carousel) {
417
- return this.insertCarouselMessage(message, options);
418
- }
419
- var element = this.messageTemplateTarget.cloneNode(true);
269
+ const createdAt = message.created_at || message.createdAt;
270
+ const element = this.messageTemplateTarget.cloneNode(true);
420
271
  element.classList.add('hellotext--webchat-message');
421
- element.style.display = 'flex';
422
- setSanitizedRichText(element.querySelector('[data-body]'), body);
423
- element.setAttribute('data-id', id);
424
272
  element.setAttribute('data-hellotext--webchat-target', 'message');
425
- this.setMessageCreatedAt(element, createdAt);
426
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
427
- if (attachments) {
428
- attachments.forEach(attachmentUrl => {
429
- var _this$messageAttachme2;
430
- var image = this.attachmentImageTarget.cloneNode(true);
431
- image.src = attachmentUrl;
432
- image.style.display = 'block';
433
- (_this$messageAttachme2 = this.messageAttachmentsContainer(element)) === null || _this$messageAttachme2 === void 0 ? void 0 : _this$messageAttachme2.appendChild(image);
434
- });
435
- }
436
- this.clearTypingIndicator();
437
- this.insertMessageElement(element);
438
- Hellotext.eventEmitter.dispatch('webchat:message:received', _objectSpread(_objectSpread({}, message), {}, {
439
- body: element.querySelector('[data-body]').innerText
440
- }));
441
- if (options.scroll !== false) {
442
- element.scrollIntoView({
443
- behavior: 'smooth'
444
- });
445
- }
446
- this.updateMessageTeaser(teaser);
447
- if (this.openValue) {
448
- this.messagesAPI.markAsSeen(id);
449
- return;
450
- }
451
- this.incrementUnreadCounter();
452
- }
453
-
454
- // TCP broadcasts may deliver the same incoming message twice before Stimulus
455
- // has connected the appended message target. Claiming the id in memory closes
456
- // that window, while the target check still drops messages rendered earlier.
457
- }, {
458
- key: "claimMessageId",
459
- value: function claimMessageId(id) {
460
- var messageTargets = this.messageTargets || [];
461
- if (this.messageIds.has(id)) return false;
462
- this.messageIds.add(id);
463
- return !messageTargets.some(element => element.dataset.id === id);
464
- }
465
- }, {
466
- key: "captureCatchUpCursor",
467
- value: function captureCatchUpCursor() {
468
- this.catchUpAfterMessageId = this.lastRenderedMessageId;
469
- }
470
- }, {
471
- key: "catchUpMessages",
472
- value: function () {
473
- var _catchUpMessages = _asyncToGenerator(function* () {
474
- var afterId = this.catchUpAfterMessageId;
475
- if (!afterId || this.fetchingCatchUpMessages) return;
476
- this.fetchingCatchUpMessages = true;
477
- try {
478
- var response = yield this.messagesAPI.catchUp(afterId);
479
- var {
480
- messages = []
481
- } = yield response.json();
482
- messages.forEach(message => this.onMessageReceived(message, {
483
- scroll: false
484
- }));
485
- this.catchUpAfterMessageId = this.lastRenderedMessageId;
486
- } finally {
487
- this.fetchingCatchUpMessages = false;
488
- }
489
- });
490
- function catchUpMessages() {
491
- return _catchUpMessages.apply(this, arguments);
492
- }
493
- return catchUpMessages;
494
- }()
495
- }, {
496
- key: "lastRenderedMessageId",
497
- get: function get() {
498
- var _messages;
499
- var messages = this.persistedMessageElements;
500
- return ((_messages = messages[messages.length - 1]) === null || _messages === void 0 ? void 0 : _messages.dataset.id) || null;
501
- }
502
- }, {
503
- key: "persistedMessageElements",
504
- get: function get() {
505
- return Array.from(this.messagesContainerTarget.querySelectorAll('.hellotext--webchat-message[data-id]'));
506
- }
507
- }, {
508
- key: "setMessageCreatedAt",
509
- value: function setMessageCreatedAt(element, createdAt) {
273
+ element.setAttribute('data-id', message.id);
510
274
  if (createdAt) {
511
275
  element.setAttribute('data-created-at', createdAt);
512
276
  }
513
- }
514
- }, {
515
- key: "insertMessageElement",
516
- value: function insertMessageElement(element) {
517
- var nextElement = this.nextMessageElementFor(element);
518
- if (nextElement) {
519
- this.messagesContainerTarget.insertBefore(element, nextElement);
277
+ element.style.removeProperty('display');
278
+ setSanitizedRichText(element.querySelector('[data-body]'), body);
279
+ if (message.state === 'received') {
280
+ element.classList.add('received');
520
281
  } else {
521
- this.messagesContainerTarget.appendChild(element);
522
- }
523
- }
524
- }, {
525
- key: "nextMessageElementFor",
526
- value: function nextMessageElementFor(element) {
527
- var createdAt = Date.parse(element.dataset.createdAt);
528
- if (Number.isNaN(createdAt)) return null;
529
- return this.persistedMessageElements.find(messageElement => {
530
- if (messageElement === element) return false;
531
- var messageCreatedAt = Date.parse(messageElement.dataset.createdAt);
532
- return !Number.isNaN(messageCreatedAt) && messageCreatedAt > createdAt;
533
- });
534
- }
535
- }, {
536
- key: "updateMessageTeaser",
537
- value: function updateMessageTeaser(teaser) {
538
- this.messageTeaserValue = teaser;
539
- if (!this.messageTeaserValue || !this.hasTeaserTarget || !this.hasInboundMessageTeaserTarget || !this.hasInboundMessageTeaserBodyTarget) {
540
- return;
541
- }
542
- this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'));
543
- this.inboundMessageTeaserBodyTarget.textContent = this.messageTeaserValue;
544
- this.inboundMessageTeaserTarget.classList.remove('hidden');
545
- this.teaserTarget.classList.toggle('invisible', this.openValue);
546
- }
547
- }, {
548
- key: "insertCarouselMessage",
549
- value: function insertCarouselMessage(message) {
550
- var _element$querySelecto;
551
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
552
- var html = message.html;
553
- var createdAt = message.created_at || message.createdAt;
554
- var element = sanitizedWebchatComponentFragment(html).firstElementChild;
555
- element.classList.add('hellotext--webchat-message');
556
- element.setAttribute('data-id', message.id);
557
- element.setAttribute('data-hellotext--webchat-target', 'message');
558
- this.setMessageCreatedAt(element, createdAt);
559
- this.localizeMessageTimestamps(element);
560
- this.clearTypingIndicator();
561
- this.insertMessageElement(element);
562
- if (options.scroll !== false) {
563
- element.scrollIntoView({
564
- behavior: 'smooth'
565
- });
566
- }
567
- Hellotext.eventEmitter.dispatch('webchat:message:received', _objectSpread(_objectSpread({}, message), {}, {
568
- body: ((_element$querySelecto = element.querySelector('[data-body]')) === null || _element$querySelecto === void 0 ? void 0 : _element$querySelecto.innerText) || ''
569
- }));
570
- this.updateMessageTeaser(message.teaser);
571
- if (this.openValue) {
572
- this.messagesAPI.markAsSeen(message.id);
573
- return;
574
- }
575
- this.incrementUnreadCounter();
576
- }
577
- }, {
578
- key: "resizeInput",
579
- value: function resizeInput() {
580
- var maxHeight = 96;
581
-
582
- // Temporarily reset height to its natural content size
583
- this.inputTarget.style.height = 'auto';
584
-
585
- // Set the height to the scrollHeight, which is the minimum height
586
- // the element needs to fit its content without a scrollbar.
587
- var scrollHeight = this.inputTarget.scrollHeight;
588
- this.inputTarget.style.height = "".concat(Math.min(scrollHeight, maxHeight), "px");
589
- }
590
- }, {
591
- key: "sendQuickReplyMessage",
592
- value: function () {
593
- var _sendQuickReplyMessage = _asyncToGenerator(function* (_ref) {
594
- var _this$dismissTeaserFo2, _cardElement$querySel;
595
- var {
596
- detail: {
597
- id,
598
- product,
599
- buttonId,
600
- body,
601
- cardElement
602
- }
603
- } = _ref;
604
- (_this$dismissTeaserFo2 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo2 === void 0 ? void 0 : _this$dismissTeaserFo2.call(this);
605
- var formData = new FormData();
606
- formData.append('message[body]', body);
607
- if (id) formData.append('message[replied_to]', id);
608
- if (product) formData.append('message[product]', product);
609
- if (buttonId) formData.append('message[button]', buttonId);
610
- formData.append('session', Hellotext.session);
611
- formData.append('locale', Locale.toString());
612
- this.appendOpeningSequenceMessageIds(formData);
613
- var element = this.buildMessageElement();
614
- 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);
615
- element.querySelector('[data-body]').innerText = body;
616
- if (attachment) {
617
- var _this$messageAttachme3;
618
- attachment.removeAttribute('width');
619
- attachment.removeAttribute('height');
620
- (_this$messageAttachme3 = this.messageAttachmentsContainer(element)) === null || _this$messageAttachme3 === void 0 ? void 0 : _this$messageAttachme3.appendChild(attachment);
621
- }
622
- if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
623
- this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
624
- } else {
625
- this.messagesContainerTarget.appendChild(element);
626
- }
627
- element.scrollIntoView({
628
- behavior: 'smooth'
629
- });
630
- this.broadcastChannel.postMessage({
631
- type: 'message:sent',
632
- element: element.outerHTML
633
- });
634
- var response = yield this.messagesAPI.create(formData);
635
- if (response.failed) {
636
- // Clear the optimistic typing indicator on failure
637
- clearTimeout(this.optimisticTypingTimeout);
638
- return this.markMessageFailedFromResponse(response, element);
639
- }
640
- var data = yield response.json();
641
- this.dispatch('set:id', {
642
- target: element,
643
- detail: data.id
644
- });
645
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
646
- this.clearRevealedOpeningSequenceMessageIds();
647
- var message = {
648
- id: data.id,
649
- body: body,
650
- attachments: attachment ? [attachment.src] : [],
651
- replied_to: id,
652
- product: product,
653
- button: buttonId,
654
- type: 'quick_reply'
655
- };
656
- Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
657
- });
658
- function sendQuickReplyMessage(_x) {
659
- return _sendQuickReplyMessage.apply(this, arguments);
282
+ element.classList.remove('received');
660
283
  }
661
- return sendQuickReplyMessage;
662
- }()
663
- }, {
664
- key: "sendTeaserQuickReply",
665
- value: function () {
666
- var _sendTeaserQuickReply = _asyncToGenerator(function* (event) {
667
- var _this$dismissTeaserFo3;
668
- event.preventDefault();
669
- event.stopPropagation();
670
- var button = event.currentTarget;
671
- var value = (button.dataset.value || '').trim();
672
- var label = [button.dataset.text, button.textContent].map(text => (text || '').trim()).find(text => text.length > 0);
673
- var text = value || label;
674
- if (!text) return;
675
- (_this$dismissTeaserFo3 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo3 === void 0 ? void 0 : _this$dismissTeaserFo3.call(this);
676
- this.show();
677
- var buttonType = (button.dataset.type || '').trim() || 'quick_reply';
678
- var formData = new FormData();
679
- formData.append('message[body]', text);
680
- formData.append('session', Hellotext.session);
681
- formData.append('locale', Locale.toString());
682
- this.appendOpeningSequenceMessageIds(formData);
683
- var element = this.buildMessageElement();
684
- element.querySelector('[data-body]').innerText = text;
685
- if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
686
- this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
687
- } else {
688
- this.messagesContainerTarget.appendChild(element);
689
- }
690
- element.scrollIntoView({
691
- behavior: 'smooth'
692
- });
693
- this.broadcastChannel.postMessage({
694
- type: 'message:sent',
695
- element: element.outerHTML
696
- });
697
- if (!this.typingIndicatorVisible) {
698
- clearTimeout(this.optimisticTypingTimeout);
699
- this.optimisticTypingTimeout = setTimeout(() => {
700
- this.showOptimisticTypingIndicator();
701
- }, this.optimisticTypingIndicatorWaitValue);
702
- }
703
- var response = yield this.messagesAPI.create(formData);
704
- if (response.failed) {
705
- clearTimeout(this.optimisticTypingTimeout);
706
- return this.markMessageFailedFromResponse(response, element);
707
- }
708
- var data = yield response.json();
709
- element.setAttribute('data-id', data.id);
710
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
711
- this.clearRevealedOpeningSequenceMessageIds();
712
- Hellotext.eventEmitter.dispatch('webchat:message:sent', {
713
- id: data.id,
714
- body: text,
715
- attachments: [],
716
- type: 'quick_reply',
717
- teaser: {
718
- text: label || text,
719
- value: value || text,
720
- type: buttonType
721
- }
722
- });
723
- if (data.conversation && data.conversation !== this.conversationIdValue) {
724
- this.conversationIdValue = data.conversation;
725
- this.webChatChannel.updateSubscriptionWith(this.conversationIdValue);
726
- }
727
- if (this.typingIndicatorVisible) {
728
- this.resetTypingIndicatorTimer();
729
- }
730
- });
731
- function sendTeaserQuickReply(_x2) {
732
- return _sendTeaserQuickReply.apply(this, arguments);
733
- }
734
- return sendTeaserQuickReply;
735
- }()
736
- }, {
737
- key: "sendMessage",
738
- value: function () {
739
- var _sendMessage = _asyncToGenerator(function* (e) {
740
- var _this$dismissTeaserFo4;
741
- var message = {
742
- body: this.inputTarget.value,
743
- attachments: this.files
744
- };
745
- if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
746
- if (e && e.target) {
747
- e.preventDefault();
748
- }
749
- return;
750
- }
751
- (_this$dismissTeaserFo4 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo4 === void 0 ? void 0 : _this$dismissTeaserFo4.call(this);
752
- var formData = new FormData();
753
- if (this.inputTarget.value.trim().length > 0) {
754
- formData.append('message[body]', this.inputTarget.value);
755
- } else {
756
- delete message.body;
757
- }
758
- this.files.forEach(file => {
759
- formData.append('message[attachments][]', file);
760
- });
761
- formData.append('session', Hellotext.session);
762
- formData.append('locale', Locale.toString());
763
- this.appendOpeningSequenceMessageIds(formData);
764
- var element = this.buildMessageElement();
765
- if (this.inputTarget.value.trim().length > 0) {
766
- element.querySelector('[data-body]').innerText = this.inputTarget.value;
767
- } else {
768
- element.querySelector('[data-message-bubble]').remove();
769
- }
770
- var attachments = this.attachmentContainerTarget.querySelectorAll('img');
771
- if (attachments.length > 0) {
772
- attachments.forEach(attachment => {
773
- var _this$messageAttachme4;
774
- (_this$messageAttachme4 = this.messageAttachmentsContainer(element)) === null || _this$messageAttachme4 === void 0 ? void 0 : _this$messageAttachme4.appendChild(attachment.cloneNode(true));
775
- });
776
- }
777
-
778
- // Insert message before typing indicator if one exists
779
- if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
780
- this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
781
- } else {
782
- this.messagesContainerTarget.appendChild(element);
783
- }
784
- element.scrollIntoView({
785
- behavior: 'smooth'
786
- });
787
- this.broadcastChannel.postMessage({
788
- type: 'message:sent',
789
- element: element.outerHTML
284
+ if (attachments) {
285
+ attachments.forEach(attachmentUrl => {
286
+ const image = this.attachmentImageTarget.cloneNode(true);
287
+ image.removeAttribute('data-hellotext--webchat-target');
288
+ image.src = attachmentUrl;
289
+ image.style.display = 'block';
290
+ this.messageAttachmentsContainer(element)?.appendChild(image);
790
291
  });
791
- this.inputTarget.value = '';
792
- this.resizeInput();
793
- this.files = [];
794
- this.attachmentInputTarget.value = '';
795
- this.attachmentContainerTarget.innerHTML = '';
796
- this.attachmentContainerTarget.style.display = 'none';
797
- this.errorMessageContainerTarget.style.display = 'none';
798
- this.focusComposeInput();
799
-
800
- // Set up optimistic typing indicator BEFORE making the API call
801
- // This prevents race conditions with server responses
802
- if (!this.typingIndicatorVisible) {
803
- clearTimeout(this.optimisticTypingTimeout);
804
- this.optimisticTypingTimeout = setTimeout(() => {
805
- this.showOptimisticTypingIndicator();
806
- }, this.optimisticTypingIndicatorWaitValue);
807
- }
808
- var response = yield this.messagesAPI.create(formData);
809
- if (response.failed) {
810
- // Clear the optimistic typing indicator on failure
811
- clearTimeout(this.optimisticTypingTimeout);
812
- return this.markMessageFailedFromResponse(response, element);
813
- }
814
- var data = yield response.json();
815
- element.setAttribute('data-id', data.id);
816
- message.id = data.id;
817
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
818
- this.clearRevealedOpeningSequenceMessageIds();
819
- Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
820
- if (data.conversation !== this.conversationIdValue) {
821
- this.conversationIdValue = data.conversation;
822
- this.webChatChannel.updateSubscriptionWith(this.conversationIdValue);
823
- }
824
-
825
- // If there's already a typing indicator visible, reset its timer
826
- if (this.typingIndicatorVisible) {
827
- this.resetTypingIndicatorTimer();
828
- }
829
- this.attachmentContainerTarget.style.display = '';
830
- });
831
- function sendMessage(_x3) {
832
- return _sendMessage.apply(this, arguments);
833
292
  }
834
- return sendMessage;
835
- }()
836
- }, {
837
- key: "buildMessageElement",
838
- value: function buildMessageElement() {
839
- var element = this.messageTemplateTarget.cloneNode(true);
840
- element.id = "hellotext--webchat--".concat(this.idValue, "--message--").concat(Date.now());
841
- element.classList.add('received');
842
- element.style.removeProperty('display');
843
- element.setAttribute('data-controller', 'hellotext--message');
844
- element.setAttribute('data-hellotext--webchat-target', 'message');
845
- this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date());
846
- return element;
847
- }
848
- }, {
849
- key: "focusCompose",
850
- value: function focusCompose(event) {
851
- var {
852
- target
853
- } = event;
854
- 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(', ');
855
- if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
856
- if (!this.focusComposeInput({
857
- moveCursorToEnd: true
858
- })) return;
859
- event.preventDefault();
293
+ element.setAttribute('data-body', body);
294
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
295
+ this.messagesContainerTarget.prepend(element);
296
+ });
297
+ this.messagesContainerTarget.scroll({
298
+ top: this.messagesContainerTarget.scrollHeight - this.oldScrollHeight,
299
+ behavior: 'instant'
300
+ });
301
+ this.fetchingNextPage = false;
302
+ }
303
+ onClickOutside(event) {
304
+ if (WebchatConfiguration.mode === modes.POPOVER && this.openValue && event.target.nodeType && this.element.contains(event.target) === false) {
305
+ this.openValue = false;
860
306
  }
861
- }, {
862
- key: "closePopoverFromHeader",
863
- value: function closePopoverFromHeader(event) {
864
- var {
865
- target
866
- } = event;
867
- if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return;
868
- event.preventDefault();
869
- this.closePopover();
307
+ }
308
+ closePopover() {
309
+ this.clearPopoverOpenAnimation();
310
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
311
+ this.openValue = false;
312
+ }
313
+ preparePopoverOpenAnimation() {
314
+ this.clearPopoverOpenAnimation();
315
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
316
+ this.popoverTarget.classList.add('hellotext--webchat-popover-opening');
317
+ this.popoverOpenAnimationTimeout = setTimeout(() => {
318
+ this.popoverTarget.classList.remove('hellotext--webchat-popover-opening');
319
+ this.popoverOpenAnimationTimeout = null;
320
+ }, POPOVER_ANIMATION_DURATION);
321
+ }
322
+ clearPopoverOpenAnimation() {
323
+ if (this.popoverOpenAnimationTimeout) {
324
+ clearTimeout(this.popoverOpenAnimationTimeout);
325
+ this.popoverOpenAnimationTimeout = null;
870
326
  }
871
- }, {
872
- key: "closePopoverOnEscape",
873
- value: function closePopoverOnEscape(event) {
874
- var _this$triggerTarget, _this$triggerTarget$f;
875
- if (event.key !== 'Escape' || !this.openValue) return;
876
- event.preventDefault();
877
- event.stopPropagation();
878
- this.closePopover();
879
- (_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);
327
+ this.popoverTarget?.classList.remove('hellotext--webchat-popover-opening');
328
+ }
329
+ onPopoverOpened() {
330
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
331
+ this.dismissTeaserForSession?.();
332
+ if (!this.onMobile) {
333
+ this.focusComposeInput();
880
334
  }
881
- }, {
882
- key: "markMessageFailedFromResponse",
883
- value: function () {
884
- var _markMessageFailedFromResponse = _asyncToGenerator(function* (response, element) {
885
- var reason = yield this.messageFailureReason(response);
886
- this.markMessageFailed(element, reason);
887
- this.broadcastChannel.postMessage({
888
- type: 'message:failed',
889
- id: element.id,
890
- reason
335
+ if (!this.scrolled) {
336
+ requestAnimationFrame(() => {
337
+ this.messagesContainerTarget.scroll({
338
+ top: this.messagesContainerTarget.scrollHeight,
339
+ behavior: 'instant'
891
340
  });
892
341
  });
893
- function markMessageFailedFromResponse(_x4, _x5) {
894
- return _markMessageFailedFromResponse.apply(this, arguments);
895
- }
896
- return markMessageFailedFromResponse;
897
- }()
898
- }, {
899
- key: "markMessageFailed",
900
- value: function markMessageFailed(element, reason) {
901
- if (!element) return;
902
- element.classList.add('failed');
903
- if (!reason) return;
904
- var timestamp = element.querySelector('[data-message-timestamp]');
905
- if (timestamp) {
906
- timestamp.textContent = reason;
907
- }
908
- }
909
- }, {
910
- key: "localizeMessageTimestamps",
911
- value: function localizeMessageTimestamps() {
912
- var _root$matches, _root$querySelectorAl;
913
- var root = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.element;
914
- if (!root) return;
915
- 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]')) || []);
916
- timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp));
917
- }
918
- }, {
919
- key: "localizeMessageTimestamp",
920
- value: function localizeMessageTimestamp(timestamp) {
921
- var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : timestamp === null || timestamp === void 0 ? void 0 : timestamp.getAttribute('datetime');
922
- if (!timestamp || !value) return;
923
- var date = value instanceof Date ? value : new Date(value);
924
- if (Number.isNaN(date.getTime())) return;
925
- timestamp.setAttribute('datetime', date.toISOString());
926
- timestamp.textContent = this.formatMessageTimestamp(date);
342
+ this.scrolled = true;
343
+ }
344
+ Hellotext.eventEmitter.dispatch('webchat:opened');
345
+ localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'opened');
346
+ if (this.messageTeaserValue) {
347
+ this.messageTeaserValue = null;
348
+ }
349
+ this.startOpeningSequence();
350
+ if (this.unreadCounterTarget.style.display === 'none') return;
351
+ this.unreadCounterTarget.style.display = 'none';
352
+ this.unreadCounterTarget.innerText = '0';
353
+ this.messagesAPI.markAsSeen();
354
+ }
355
+ onPopoverClosed() {
356
+ this.clearPopoverOpenAnimation();
357
+ Hellotext.eventEmitter.dispatch('webchat:closed');
358
+ localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'closed');
359
+ }
360
+ onMessageReaction(message) {
361
+ const {
362
+ message: messageId,
363
+ reaction,
364
+ type
365
+ } = message;
366
+ const messageElement = this.messageTargets.find(element => element.dataset.id === messageId);
367
+ const reactionsContainer = messageElement.querySelector('[data-reactions]');
368
+ if (type === 'reaction.destroy') {
369
+ const reactionElement = reactionsContainer.querySelector(`[data-id="${reaction.id}"]`);
370
+ return reactionElement.remove();
371
+ }
372
+ if (reactionsContainer.querySelector(`[data-id="${reaction.id}"]`)) {
373
+ const reactionElement = reactionsContainer.querySelector(`[data-id="${reaction.id}"]`);
374
+ reactionElement.innerText = reaction.emoji;
375
+ } else {
376
+ const reactionElement = document.createElement('span');
377
+ reactionElement.innerText = reaction.emoji;
378
+ reactionElement.setAttribute('data-id', reaction.id);
379
+ reactionsContainer.appendChild(reactionElement);
927
380
  }
928
- }, {
929
- key: "formatMessageTimestamp",
930
- value: function formatMessageTimestamp(date) {
931
- return this.constructor.messageTimestampFormatterFor(Locale.toString()).format(date);
381
+ }
382
+ onMessageReceived(message, options = {}) {
383
+ const {
384
+ id,
385
+ body,
386
+ attachments,
387
+ teaser
388
+ } = message;
389
+ const createdAt = message.created_at || message.createdAt;
390
+ if (!this.claimMessageId(id)) return;
391
+ this.hideTeaser?.();
392
+ if (message.carousel) {
393
+ return this.insertCarouselMessage(message, options);
394
+ }
395
+ const element = this.messageTemplateTarget.cloneNode(true);
396
+ element.classList.add('hellotext--webchat-message');
397
+ element.style.display = 'flex';
398
+ setSanitizedRichText(element.querySelector('[data-body]'), body);
399
+ element.setAttribute('data-id', id);
400
+ element.setAttribute('data-hellotext--webchat-target', 'message');
401
+ this.setMessageCreatedAt(element, createdAt);
402
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
403
+ if (attachments) {
404
+ attachments.forEach(attachmentUrl => {
405
+ const image = this.attachmentImageTarget.cloneNode(true);
406
+ image.src = attachmentUrl;
407
+ image.style.display = 'block';
408
+ this.messageAttachmentsContainer(element)?.appendChild(image);
409
+ });
932
410
  }
933
- }, {
934
- key: "messageFailureReason",
935
- value: function () {
936
- var _messageFailureReason = _asyncToGenerator(function* (response) {
937
- var nativeResponse = (response === null || response === void 0 ? void 0 : response.data) || (response === null || response === void 0 ? void 0 : response.response);
938
- var fallback = (nativeResponse === null || nativeResponse === void 0 ? void 0 : nativeResponse.statusText) || 'Message failed';
939
- try {
940
- var _jsonResponse$json;
941
- var jsonResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
942
- 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);
943
- var reason = this.messageFailureReasonFromPayload(payload);
944
- if (reason) return reason;
945
- } catch (_) {
946
- // Fall through to text parsing/fallback. Some stores strip content-type or
947
- // return non-JSON failures, so the timestamp still needs a useful reason.
948
- }
949
- try {
950
- var _textResponse$text;
951
- var textResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
952
- 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);
953
- return this.messageFailureReasonFromText(text) || fallback;
954
- } catch (_) {
955
- return fallback;
956
- }
411
+ this.clearTypingIndicator();
412
+ this.insertMessageElement(element);
413
+ Hellotext.eventEmitter.dispatch('webchat:message:received', {
414
+ ...message,
415
+ body: element.querySelector('[data-body]').innerText
416
+ });
417
+ if (options.scroll !== false) {
418
+ element.scrollIntoView({
419
+ behavior: 'smooth'
957
420
  });
958
- function messageFailureReason(_x6) {
959
- return _messageFailureReason.apply(this, arguments);
960
- }
961
- return messageFailureReason;
962
- }()
963
- }, {
964
- key: "messageFailureReasonFromText",
965
- value: function messageFailureReasonFromText(text) {
966
- if (typeof text !== 'string') return null;
967
- var value = text.trim();
968
- if (!value || value.startsWith('<')) return null;
969
- try {
970
- return this.messageFailureReasonFromPayload(JSON.parse(value)) || value;
971
- } catch (_) {
972
- return value;
973
- }
974
421
  }
975
- }, {
976
- key: "messageFailureReasonFromPayload",
977
- value: function messageFailureReasonFromPayload(payload) {
978
- var _payload$error, _payload$errors, _payload$errors2, _payload$errors2$, _payload$errors3, _payload$errors3$;
979
- if (!payload) return null;
980
- 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);
422
+ this.updateMessageTeaser(teaser);
423
+ if (this.openValue) {
424
+ this.messagesAPI.markAsSeen(id);
425
+ return;
981
426
  }
982
- }, {
983
- key: "messageAttachmentsContainer",
984
- value: function messageAttachmentsContainer(element) {
985
- return element.querySelector('[data-attachments-container], [data-attachment-container]');
427
+ this.incrementUnreadCounter();
428
+ }
429
+
430
+ // TCP broadcasts may deliver the same incoming message twice before Stimulus
431
+ // has connected the appended message target. Claiming the id in memory closes
432
+ // that window, while the target check still drops messages rendered earlier.
433
+ claimMessageId(id) {
434
+ const messageTargets = this.messageTargets || [];
435
+ if (this.messageIds.has(id)) return false;
436
+ this.messageIds.add(id);
437
+ return !messageTargets.some(element => element.dataset.id === id);
438
+ }
439
+ captureCatchUpCursor() {
440
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
441
+ }
442
+ async catchUpMessages() {
443
+ const afterId = this.catchUpAfterMessageId;
444
+ if (!afterId || this.fetchingCatchUpMessages) return;
445
+ this.fetchingCatchUpMessages = true;
446
+ try {
447
+ const response = await this.messagesAPI.catchUp(afterId);
448
+ const {
449
+ messages = []
450
+ } = await response.json();
451
+ messages.forEach(message => this.onMessageReceived(message, {
452
+ scroll: false
453
+ }));
454
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
455
+ } finally {
456
+ this.fetchingCatchUpMessages = false;
986
457
  }
987
- }, {
988
- key: "incrementUnreadCounter",
989
- value: function incrementUnreadCounter() {
990
- this.unreadCounterTarget.style.display = 'flex';
991
- var unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1;
992
- this.unreadCounterTarget.innerText = Math.min(unreadCount, 9);
458
+ }
459
+ get lastRenderedMessageId() {
460
+ const messages = this.persistedMessageElements;
461
+ return messages[messages.length - 1]?.dataset.id || null;
462
+ }
463
+ get persistedMessageElements() {
464
+ return Array.from(this.messagesContainerTarget.querySelectorAll('.hellotext--webchat-message[data-id]'));
465
+ }
466
+ setMessageCreatedAt(element, createdAt) {
467
+ if (createdAt) {
468
+ element.setAttribute('data-created-at', createdAt);
993
469
  }
994
- }, {
995
- key: "openAttachment",
996
- value: function openAttachment() {
997
- this.attachmentInputTarget.click();
470
+ }
471
+ insertMessageElement(element) {
472
+ const nextElement = this.nextMessageElementFor(element);
473
+ if (nextElement) {
474
+ this.messagesContainerTarget.insertBefore(element, nextElement);
475
+ } else {
476
+ this.messagesContainerTarget.appendChild(element);
998
477
  }
999
- }, {
1000
- key: "onFileInputChange",
1001
- value: function onFileInputChange() {
1002
- this.errorMessageContainerTarget.style.display = 'none';
1003
- var newFiles = Array.from(this.attachmentInputTarget.files);
1004
- this.attachmentInputTarget.value = '';
1005
- var fileMaxSizeTooMuch = newFiles.find(file => {
1006
- var type = file.type.split('/')[0];
1007
- if (['image', 'video', 'audio'].includes(type)) {
1008
- return this.mediaValue[type].max_size < file.size;
1009
- } else {
1010
- return this.mediaValue.document.max_size < file.size;
1011
- }
478
+ }
479
+ nextMessageElementFor(element) {
480
+ const createdAt = Date.parse(element.dataset.createdAt);
481
+ if (Number.isNaN(createdAt)) return null;
482
+ return this.persistedMessageElements.find(messageElement => {
483
+ if (messageElement === element) return false;
484
+ const messageCreatedAt = Date.parse(messageElement.dataset.createdAt);
485
+ return !Number.isNaN(messageCreatedAt) && messageCreatedAt > createdAt;
486
+ });
487
+ }
488
+ updateMessageTeaser(teaser) {
489
+ this.messageTeaserValue = teaser;
490
+ if (!this.messageTeaserValue || !this.hasTeaserTarget || !this.hasInboundMessageTeaserTarget || !this.hasInboundMessageTeaserBodyTarget) {
491
+ return;
492
+ }
493
+ this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'));
494
+ this.inboundMessageTeaserBodyTarget.textContent = this.messageTeaserValue;
495
+ this.inboundMessageTeaserTarget.classList.remove('hidden');
496
+ this.teaserTarget.classList.toggle('invisible', this.openValue);
497
+ }
498
+ insertCarouselMessage(message, options = {}) {
499
+ const html = message.html;
500
+ const createdAt = message.created_at || message.createdAt;
501
+ const element = sanitizedWebchatComponentFragment(html).firstElementChild;
502
+ element.classList.add('hellotext--webchat-message');
503
+ element.setAttribute('data-id', message.id);
504
+ element.setAttribute('data-hellotext--webchat-target', 'message');
505
+ this.setMessageCreatedAt(element, createdAt);
506
+ this.localizeMessageTimestamps(element);
507
+ this.clearTypingIndicator();
508
+ this.insertMessageElement(element);
509
+ if (options.scroll !== false) {
510
+ element.scrollIntoView({
511
+ behavior: 'smooth'
1012
512
  });
1013
- if (fileMaxSizeTooMuch) {
1014
- var type = fileMaxSizeTooMuch.type.split('/')[0];
1015
- var mediaType = ['image', 'audio', 'video'].includes(type) ? type : 'document';
1016
- this.errorMessageContainerTarget.innerText = this.fileSizeErrorMessageValue.replace('%{limit}', this.byteToMegabyte(this.mediaValue[mediaType].max_size));
1017
- return this.errorMessageContainerTarget.style.display = 'block';
1018
- }
1019
- this.files = [...this.files, ...newFiles];
1020
- this.errorMessageContainerTarget.innerText = '';
1021
- newFiles.forEach(file => this.createAttachmentElement(file));
1022
- this.focusComposeInput();
1023
- }
1024
- }, {
1025
- key: "createAttachmentElement",
1026
- value: function createAttachmentElement(file) {
1027
- var element = this.attachmentElement();
1028
- this.attachmentContainerTarget.style.display = '';
1029
- element.setAttribute('data-name', file.name);
1030
- if (file.type.startsWith('image/')) {
1031
- var thumbnail = this.attachmentImageTarget.cloneNode(true);
1032
- thumbnail.src = URL.createObjectURL(file);
1033
- thumbnail.style.display = 'block';
1034
- element.appendChild(thumbnail);
1035
- this.attachmentContainerTarget.appendChild(element);
1036
- this.attachmentContainerTarget.style.display = 'flex';
1037
- } else {
1038
- var main = element.querySelector('main');
1039
- main.style.height = '5rem';
1040
- main.style.borderRadius = '0.375rem';
1041
- main.style.backgroundColor = '#e5e7eb';
1042
- main.style.padding = '0.25rem';
1043
- element.querySelector('p[data-attachment-name]').innerText = file.name;
1044
- this.attachmentContainerTarget.appendChild(element);
1045
- this.attachmentContainerTarget.style.display = 'flex';
1046
- }
1047
513
  }
1048
- }, {
1049
- key: "removeAttachment",
1050
- value: function removeAttachment(_ref2) {
1051
- var {
1052
- currentTarget
1053
- } = _ref2;
1054
- var attachment = currentTarget.closest("[data-hellotext--webchat-target='attachment']");
1055
- this.files = this.files.filter(file => file.name !== attachment.dataset.name);
1056
- this.attachmentInputTarget.value = '';
1057
- attachment.remove();
1058
- this.focusComposeInput();
514
+ Hellotext.eventEmitter.dispatch('webchat:message:received', {
515
+ ...message,
516
+ body: element.querySelector('[data-body]')?.innerText || ''
517
+ });
518
+ this.updateMessageTeaser(message.teaser);
519
+ if (this.openValue) {
520
+ this.messagesAPI.markAsSeen(message.id);
521
+ return;
1059
522
  }
1060
- }, {
1061
- key: "attachmentTargetDisconnected",
1062
- value: function attachmentTargetDisconnected() {
1063
- if (this.attachmentTargets.length === 0) {
1064
- this.attachmentContainerTarget.innerHTML = '';
1065
- this.attachmentContainerTarget.style.display = 'none';
1066
- }
523
+ this.incrementUnreadCounter();
524
+ }
525
+ resizeInput() {
526
+ const maxHeight = 96;
527
+
528
+ // Temporarily reset height to its natural content size
529
+ this.inputTarget.style.height = 'auto';
530
+
531
+ // Set the height to the scrollHeight, which is the minimum height
532
+ // the element needs to fit its content without a scrollbar.
533
+ const scrollHeight = this.inputTarget.scrollHeight;
534
+ this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
535
+ }
536
+ async sendQuickReplyMessage({
537
+ detail: {
538
+ id,
539
+ product,
540
+ buttonId,
541
+ body,
542
+ cardElement
543
+ }
544
+ }) {
545
+ this.dismissTeaserForSession?.();
546
+ const formData = new FormData();
547
+ formData.append('message[body]', body);
548
+ if (id) formData.append('message[replied_to]', id);
549
+ if (product) formData.append('message[product]', product);
550
+ if (buttonId) formData.append('message[button]', buttonId);
551
+ formData.append('session', Hellotext.session);
552
+ formData.append('locale', Locale.toString());
553
+ this.appendOpeningSequenceMessageIds(formData);
554
+ const element = this.buildMessageElement();
555
+ const attachment = cardElement?.querySelector('img')?.cloneNode(true);
556
+ element.querySelector('[data-body]').innerText = body;
557
+ if (attachment) {
558
+ attachment.removeAttribute('width');
559
+ attachment.removeAttribute('height');
560
+ this.messageAttachmentsContainer(element)?.appendChild(attachment);
561
+ }
562
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
563
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
564
+ } else {
565
+ this.messagesContainerTarget.appendChild(element);
566
+ }
567
+ element.scrollIntoView({
568
+ behavior: 'smooth'
569
+ });
570
+ this.broadcastChannel.postMessage({
571
+ type: 'message:sent',
572
+ element: element.outerHTML
573
+ });
574
+ const response = await this.messagesAPI.create(formData);
575
+ if (response.failed) {
576
+ // Clear the optimistic typing indicator on failure
577
+ clearTimeout(this.optimisticTypingTimeout);
578
+ return this.markMessageFailedFromResponse(response, element);
579
+ }
580
+ const data = await response.json();
581
+ this.dispatch('set:id', {
582
+ target: element,
583
+ detail: data.id
584
+ });
585
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
586
+ this.clearRevealedOpeningSequenceMessageIds();
587
+ const message = {
588
+ id: data.id,
589
+ body: body,
590
+ attachments: attachment ? [attachment.src] : [],
591
+ replied_to: id,
592
+ product: product,
593
+ button: buttonId,
594
+ type: 'quick_reply'
595
+ };
596
+ Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
597
+ }
598
+ async sendTeaserQuickReply(event) {
599
+ event.preventDefault();
600
+ event.stopPropagation();
601
+ const button = event.currentTarget;
602
+ const value = (button.dataset.value || '').trim();
603
+ const label = [button.dataset.text, button.textContent].map(text => (text || '').trim()).find(text => text.length > 0);
604
+ const text = value || label;
605
+ if (!text) return;
606
+ this.dismissTeaserForSession?.();
607
+ this.show();
608
+ const buttonType = (button.dataset.type || '').trim() || 'quick_reply';
609
+ const formData = new FormData();
610
+ formData.append('message[body]', text);
611
+ formData.append('session', Hellotext.session);
612
+ formData.append('locale', Locale.toString());
613
+ this.appendOpeningSequenceMessageIds(formData);
614
+ const element = this.buildMessageElement();
615
+ element.querySelector('[data-body]').innerText = text;
616
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
617
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
618
+ } else {
619
+ this.messagesContainerTarget.appendChild(element);
620
+ }
621
+ element.scrollIntoView({
622
+ behavior: 'smooth'
623
+ });
624
+ this.broadcastChannel.postMessage({
625
+ type: 'message:sent',
626
+ element: element.outerHTML
627
+ });
628
+ if (!this.typingIndicatorVisible) {
629
+ clearTimeout(this.optimisticTypingTimeout);
630
+ this.optimisticTypingTimeout = setTimeout(() => {
631
+ this.showOptimisticTypingIndicator();
632
+ }, this.optimisticTypingIndicatorWaitValue);
1067
633
  }
1068
- }, {
1069
- key: "attachmentElement",
1070
- value: function attachmentElement() {
1071
- var element = this.attachmentTemplateTarget.cloneNode(true);
1072
- element.removeAttribute('hidden');
1073
- element.style.display = 'flex';
1074
- element.setAttribute('data-hellotext--webchat-target', 'attachment');
1075
- return element;
634
+ const response = await this.messagesAPI.create(formData);
635
+ if (response.failed) {
636
+ clearTimeout(this.optimisticTypingTimeout);
637
+ return this.markMessageFailedFromResponse(response, element);
638
+ }
639
+ const data = await response.json();
640
+ element.setAttribute('data-id', data.id);
641
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
642
+ this.clearRevealedOpeningSequenceMessageIds();
643
+ Hellotext.eventEmitter.dispatch('webchat:message:sent', {
644
+ id: data.id,
645
+ body: text,
646
+ attachments: [],
647
+ type: 'quick_reply',
648
+ teaser: {
649
+ text: label || text,
650
+ value: value || text,
651
+ type: buttonType
652
+ }
653
+ });
654
+ if (data.conversation && data.conversation !== this.conversationIdValue) {
655
+ this.conversationIdValue = data.conversation;
656
+ this.webChatChannel.updateSubscriptionWith(this.conversationIdValue);
657
+ }
658
+ if (this.typingIndicatorVisible) {
659
+ this.resetTypingIndicatorTimer();
1076
660
  }
1077
- }, {
1078
- key: "onEmojiSelect",
1079
- value: function onEmojiSelect(_ref3) {
1080
- var {
1081
- detail: emoji
1082
- } = _ref3;
1083
- var value = this.inputTarget.value;
1084
- var start = this.inputTarget.selectionStart;
1085
- var end = this.inputTarget.selectionEnd;
1086
- this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end);
1087
- this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
1088
- this.focusComposeInput();
661
+ }
662
+ async sendMessage(e) {
663
+ const message = {
664
+ body: this.inputTarget.value,
665
+ attachments: this.files
666
+ };
667
+ if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
668
+ if (e && e.target) {
669
+ e.preventDefault();
670
+ }
671
+ return;
672
+ }
673
+ this.dismissTeaserForSession?.();
674
+ const formData = new FormData();
675
+ if (this.inputTarget.value.trim().length > 0) {
676
+ formData.append('message[body]', this.inputTarget.value);
677
+ } else {
678
+ delete message.body;
679
+ }
680
+ this.files.forEach(file => {
681
+ formData.append('message[attachments][]', file);
682
+ });
683
+ formData.append('session', Hellotext.session);
684
+ formData.append('locale', Locale.toString());
685
+ this.appendOpeningSequenceMessageIds(formData);
686
+ const element = this.buildMessageElement();
687
+ if (this.inputTarget.value.trim().length > 0) {
688
+ element.querySelector('[data-body]').innerText = this.inputTarget.value;
689
+ } else {
690
+ element.querySelector('[data-message-bubble]').remove();
691
+ }
692
+ const attachments = this.attachmentContainerTarget.querySelectorAll('img');
693
+ if (attachments.length > 0) {
694
+ attachments.forEach(attachment => {
695
+ this.messageAttachmentsContainer(element)?.appendChild(attachment.cloneNode(true));
696
+ });
1089
697
  }
1090
- }, {
1091
- key: "focusComposeInput",
1092
- value: function focusComposeInput() {
1093
- var {
1094
- moveCursorToEnd = false
1095
- } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1096
- if (!this.shouldAutofocusCompose) return false;
1097
- if (this.hasInputTarget === false) return false;
1098
- if (this.hasInputTarget === undefined && !this.inputTarget) return false;
1099
- this.inputTarget.focus();
1100
- if (moveCursorToEnd && typeof this.inputTarget.selectionStart === 'number') {
1101
- var position = this.inputTarget.value.length;
1102
- this.inputTarget.setSelectionRange(position, position);
1103
- }
1104
- return true;
698
+
699
+ // Insert message before typing indicator if one exists
700
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
701
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
702
+ } else {
703
+ this.messagesContainerTarget.appendChild(element);
704
+ }
705
+ element.scrollIntoView({
706
+ behavior: 'smooth'
707
+ });
708
+ this.broadcastChannel.postMessage({
709
+ type: 'message:sent',
710
+ element: element.outerHTML
711
+ });
712
+ this.inputTarget.value = '';
713
+ this.resizeInput();
714
+ this.files = [];
715
+ this.attachmentInputTarget.value = '';
716
+ this.attachmentContainerTarget.innerHTML = '';
717
+ this.attachmentContainerTarget.style.display = 'none';
718
+ this.errorMessageContainerTarget.style.display = 'none';
719
+ this.focusComposeInput();
720
+
721
+ // Set up optimistic typing indicator BEFORE making the API call
722
+ // This prevents race conditions with server responses
723
+ if (!this.typingIndicatorVisible) {
724
+ clearTimeout(this.optimisticTypingTimeout);
725
+ this.optimisticTypingTimeout = setTimeout(() => {
726
+ this.showOptimisticTypingIndicator();
727
+ }, this.optimisticTypingIndicatorWaitValue);
1105
728
  }
1106
- }, {
1107
- key: "byteToMegabyte",
1108
- value: function byteToMegabyte(bytes) {
1109
- return Math.ceil(bytes / 1024 / 1024);
729
+ const response = await this.messagesAPI.create(formData);
730
+ if (response.failed) {
731
+ // Clear the optimistic typing indicator on failure
732
+ clearTimeout(this.optimisticTypingTimeout);
733
+ return this.markMessageFailedFromResponse(response, element);
734
+ }
735
+ const data = await response.json();
736
+ element.setAttribute('data-id', data.id);
737
+ message.id = data.id;
738
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
739
+ this.clearRevealedOpeningSequenceMessageIds();
740
+ Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
741
+ if (data.conversation !== this.conversationIdValue) {
742
+ this.conversationIdValue = data.conversation;
743
+ this.webChatChannel.updateSubscriptionWith(this.conversationIdValue);
1110
744
  }
1111
- }, {
1112
- key: "middlewares",
1113
- get: function get() {
1114
- return [offset(this.offsetValue), shift({
1115
- padding: this.paddingValue
1116
- }), flip()];
745
+
746
+ // If there's already a typing indicator visible, reset its timer
747
+ if (this.typingIndicatorVisible) {
748
+ this.resetTypingIndicatorTimer();
1117
749
  }
1118
- }, {
1119
- key: "shouldOpenOnMount",
1120
- get: function get() {
1121
- return localStorage.getItem("hellotext--webchat--".concat(this.idValue)) === 'opened' && !this.onMobile;
750
+ this.attachmentContainerTarget.style.display = '';
751
+ }
752
+ buildMessageElement() {
753
+ const element = this.messageTemplateTarget.cloneNode(true);
754
+ element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`;
755
+ element.classList.add('received');
756
+ element.style.removeProperty('display');
757
+ element.setAttribute('data-controller', 'hellotext--message');
758
+ element.setAttribute('data-hellotext--webchat-target', 'message');
759
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date());
760
+ return element;
761
+ }
762
+ focusCompose(event) {
763
+ const {
764
+ target
765
+ } = event;
766
+ 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(', ');
767
+ if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
768
+ if (!this.focusComposeInput({
769
+ moveCursorToEnd: true
770
+ })) return;
771
+ event.preventDefault();
772
+ }
773
+ closePopoverFromHeader(event) {
774
+ const {
775
+ target
776
+ } = event;
777
+ if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return;
778
+ event.preventDefault();
779
+ this.closePopover();
780
+ }
781
+ closePopoverOnEscape(event) {
782
+ if (event.key !== 'Escape' || !this.openValue) return;
783
+ event.preventDefault();
784
+ event.stopPropagation();
785
+ this.closePopover();
786
+ this.triggerTarget?.focus?.();
787
+ }
788
+ async markMessageFailedFromResponse(response, element) {
789
+ const reason = await this.messageFailureReason(response);
790
+ this.markMessageFailed(element, reason);
791
+ this.broadcastChannel.postMessage({
792
+ type: 'message:failed',
793
+ id: element.id,
794
+ reason
795
+ });
796
+ }
797
+ markMessageFailed(element, reason) {
798
+ if (!element) return;
799
+ element.classList.add('failed');
800
+ if (!reason) return;
801
+ const timestamp = element.querySelector('[data-message-timestamp]');
802
+ if (timestamp) {
803
+ timestamp.textContent = reason;
1122
804
  }
1123
- }, {
1124
- key: "shouldAutofocusCompose",
1125
- get: function get() {
1126
- return !this.usesVirtualKeyboard;
805
+ }
806
+ localizeMessageTimestamps(root = this.element) {
807
+ if (!root) return;
808
+ const timestamps = root.matches?.('time[datetime][data-message-timestamp]') ? [root] : Array.from(root.querySelectorAll?.('time[datetime][data-message-timestamp]') || []);
809
+ timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp));
810
+ }
811
+ localizeMessageTimestamp(timestamp, value = timestamp?.getAttribute('datetime')) {
812
+ if (!timestamp || !value) return;
813
+ const date = value instanceof Date ? value : new Date(value);
814
+ if (Number.isNaN(date.getTime())) return;
815
+ timestamp.setAttribute('datetime', date.toISOString());
816
+ timestamp.textContent = this.formatMessageTimestamp(date);
817
+ }
818
+ formatMessageTimestamp(date) {
819
+ return this.constructor.messageTimestampFormatterFor(Locale.toString()).format(date);
820
+ }
821
+ static messageTimestampFormatterFor(locale) {
822
+ const key = locale || 'default';
823
+ if (!this.messageTimestampFormatters[key]) {
824
+ this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale);
1127
825
  }
1128
- }, {
1129
- key: "usesVirtualKeyboard",
1130
- get: function get() {
1131
- var _navigator$userAgentD;
1132
- if (typeof navigator === 'undefined') return false;
1133
- var userAgent = navigator.userAgent || '';
1134
- var isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
1135
- var isMobileUserAgent = MOBILE_USER_AGENT_PATTERN.test(userAgent);
1136
- var isUserAgentDataMobile = ((_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.mobile) === true;
1137
- return isMobileUserAgent || isIPadOS || isUserAgentDataMobile || this.hasTouchOnlyPointer;
826
+ return this.messageTimestampFormatters[key];
827
+ }
828
+ static buildMessageTimestampFormatter(locale) {
829
+ try {
830
+ return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
831
+ } catch (_) {
832
+ return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1138
833
  }
1139
- }, {
1140
- key: "hasTouchOnlyPointer",
1141
- get: function get() {
1142
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1143
- return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(hover: none)').matches;
834
+ }
835
+ async messageFailureReason(response) {
836
+ const nativeResponse = response?.data || response?.response;
837
+ const fallback = nativeResponse?.statusText || 'Message failed';
838
+ try {
839
+ const jsonResponse = nativeResponse?.clone ? nativeResponse.clone() : nativeResponse;
840
+ const payload = await jsonResponse?.json?.();
841
+ const reason = this.messageFailureReasonFromPayload(payload);
842
+ if (reason) return reason;
843
+ } catch (_) {
844
+ // Fall through to text parsing/fallback. Some stores strip content-type or
845
+ // return non-JSON failures, so the timestamp still needs a useful reason.
846
+ }
847
+ try {
848
+ const textResponse = nativeResponse?.clone ? nativeResponse.clone() : nativeResponse;
849
+ const text = await textResponse?.text?.();
850
+ return this.messageFailureReasonFromText(text) || fallback;
851
+ } catch (_) {
852
+ return fallback;
1144
853
  }
1145
- }, {
1146
- key: "onMobile",
1147
- get: function get() {
1148
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1149
- return window.matchMedia("(max-width: ".concat(this.fullScreenThresholdValue, "px)")).matches;
854
+ }
855
+ messageFailureReasonFromText(text) {
856
+ if (typeof text !== 'string') return null;
857
+ const value = text.trim();
858
+ if (!value || value.startsWith('<')) return null;
859
+ try {
860
+ return this.messageFailureReasonFromPayload(JSON.parse(value)) || value;
861
+ } catch (_) {
862
+ return value;
1150
863
  }
1151
- }], [{
1152
- key: "messageTimestampFormatterFor",
1153
- value: function messageTimestampFormatterFor(locale) {
1154
- var key = locale || 'default';
1155
- if (!this.messageTimestampFormatters[key]) {
1156
- this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale);
1157
- }
1158
- return this.messageTimestampFormatters[key];
864
+ }
865
+ messageFailureReasonFromPayload(payload) {
866
+ if (!payload) return null;
867
+ return [payload.error?.message, payload.message, payload.errors?.message, payload.errors?.[0]?.message, payload.errors?.[0]?.description].find(reason => typeof reason === 'string' && reason.trim().length > 0);
868
+ }
869
+ messageAttachmentsContainer(element) {
870
+ return element.querySelector('[data-attachments-container], [data-attachment-container]');
871
+ }
872
+ incrementUnreadCounter() {
873
+ this.unreadCounterTarget.style.display = 'flex';
874
+ const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1;
875
+ this.unreadCounterTarget.innerText = Math.min(unreadCount, 9);
876
+ }
877
+ openAttachment() {
878
+ this.attachmentInputTarget.click();
879
+ }
880
+ onFileInputChange() {
881
+ this.errorMessageContainerTarget.style.display = 'none';
882
+ const newFiles = Array.from(this.attachmentInputTarget.files);
883
+ this.attachmentInputTarget.value = '';
884
+ const fileMaxSizeTooMuch = newFiles.find(file => {
885
+ const type = file.type.split('/')[0];
886
+ if (['image', 'video', 'audio'].includes(type)) {
887
+ return this.mediaValue[type].max_size < file.size;
888
+ } else {
889
+ return this.mediaValue.document.max_size < file.size;
890
+ }
891
+ });
892
+ if (fileMaxSizeTooMuch) {
893
+ const type = fileMaxSizeTooMuch.type.split('/')[0];
894
+ const mediaType = ['image', 'audio', 'video'].includes(type) ? type : 'document';
895
+ this.errorMessageContainerTarget.innerText = this.fileSizeErrorMessageValue.replace('%{limit}', this.byteToMegabyte(this.mediaValue[mediaType].max_size));
896
+ return this.errorMessageContainerTarget.style.display = 'block';
897
+ }
898
+ this.files = [...this.files, ...newFiles];
899
+ this.errorMessageContainerTarget.innerText = '';
900
+ newFiles.forEach(file => this.createAttachmentElement(file));
901
+ this.focusComposeInput();
902
+ }
903
+ createAttachmentElement(file) {
904
+ const element = this.attachmentElement();
905
+ this.attachmentContainerTarget.style.display = '';
906
+ element.setAttribute('data-name', file.name);
907
+ if (file.type.startsWith('image/')) {
908
+ const thumbnail = this.attachmentImageTarget.cloneNode(true);
909
+ thumbnail.src = URL.createObjectURL(file);
910
+ thumbnail.style.display = 'block';
911
+ element.appendChild(thumbnail);
912
+ this.attachmentContainerTarget.appendChild(element);
913
+ this.attachmentContainerTarget.style.display = 'flex';
914
+ } else {
915
+ const main = element.querySelector('main');
916
+ main.style.height = '5rem';
917
+ main.style.borderRadius = '0.375rem';
918
+ main.style.backgroundColor = '#e5e7eb';
919
+ main.style.padding = '0.25rem';
920
+ element.querySelector('p[data-attachment-name]').innerText = file.name;
921
+ this.attachmentContainerTarget.appendChild(element);
922
+ this.attachmentContainerTarget.style.display = 'flex';
1159
923
  }
1160
- }, {
1161
- key: "buildMessageTimestampFormatter",
1162
- value: function buildMessageTimestampFormatter(locale) {
1163
- try {
1164
- return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1165
- } catch (_) {
1166
- return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1167
- }
924
+ }
925
+ removeAttachment({
926
+ currentTarget
927
+ }) {
928
+ const attachment = currentTarget.closest("[data-hellotext--webchat-target='attachment']");
929
+ this.files = this.files.filter(file => file.name !== attachment.dataset.name);
930
+ this.attachmentInputTarget.value = '';
931
+ attachment.remove();
932
+ this.focusComposeInput();
933
+ }
934
+ attachmentTargetDisconnected() {
935
+ if (this.attachmentTargets.length === 0) {
936
+ this.attachmentContainerTarget.innerHTML = '';
937
+ this.attachmentContainerTarget.style.display = 'none';
1168
938
  }
1169
- }]);
1170
- return _default;
1171
- }(Controller);
1172
- _default.messageTimestampFormatters = {};
1173
- _default.values = {
1174
- id: String,
1175
- conversationId: String,
1176
- media: Object,
1177
- fileSizeErrorMessage: String,
1178
- placement: {
1179
- type: String,
1180
- default: 'bottom-end'
1181
- },
1182
- open: {
1183
- type: Boolean,
1184
- default: false
1185
- },
1186
- autoPlacement: {
1187
- type: Boolean,
1188
- default: false
1189
- },
1190
- disabled: {
1191
- type: Boolean,
1192
- default: false
1193
- },
1194
- nextPage: {
1195
- type: Number,
1196
- default: undefined
1197
- },
1198
- fullScreenThreshold: {
1199
- type: Number,
1200
- default: 1024
1201
- },
1202
- typingIndicatorKeepAlive: {
1203
- type: Number,
1204
- default: 30000
1205
- },
1206
- // 30 seconds
1207
- offset: {
1208
- type: Number,
1209
- default: 24
1210
- },
1211
- padding: {
1212
- type: Number,
1213
- default: 24
1214
- },
1215
- optimisticTypingIndicatorWait: {
1216
- type: Number,
1217
- default: 1000
1218
- },
1219
- // 1 second,
1220
- teaser: Object,
1221
- messageTeaser: String,
1222
- behaviour: Object
1223
- };
1224
- _default.classes = ['fadeOut'];
1225
- _default.targets = ['trigger', 'popover', 'input', 'attachmentInput', 'attachmentButton', 'errorMessageContainer', 'attachmentTemplate', 'attachmentContainer', 'attachment', 'messageTemplate', 'messagesContainer', 'title', 'attachmentImage', 'footer', 'toolbar', 'message', 'unreadCounter', 'typingIndicator', 'typingIndicatorTemplate', 'teaser', 'teaserMessage', 'inboundMessageTeaser', 'inboundMessageTeaserBody', 'openingSequence', 'openingSequenceMessage'];
1226
- export { _default as default };
939
+ }
940
+ attachmentElement() {
941
+ const element = this.attachmentTemplateTarget.cloneNode(true);
942
+ element.removeAttribute('hidden');
943
+ element.style.display = 'flex';
944
+ element.setAttribute('data-hellotext--webchat-target', 'attachment');
945
+ return element;
946
+ }
947
+ onEmojiSelect({
948
+ detail: emoji
949
+ }) {
950
+ const value = this.inputTarget.value;
951
+ const start = this.inputTarget.selectionStart;
952
+ const end = this.inputTarget.selectionEnd;
953
+ this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end);
954
+ this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
955
+ this.focusComposeInput();
956
+ }
957
+ focusComposeInput({
958
+ moveCursorToEnd = false
959
+ } = {}) {
960
+ if (!this.shouldAutofocusCompose) return false;
961
+ if (this.hasInputTarget === false) return false;
962
+ if (this.hasInputTarget === undefined && !this.inputTarget) return false;
963
+ this.inputTarget.focus();
964
+ if (moveCursorToEnd && typeof this.inputTarget.selectionStart === 'number') {
965
+ const position = this.inputTarget.value.length;
966
+ this.inputTarget.setSelectionRange(position, position);
967
+ }
968
+ return true;
969
+ }
970
+ byteToMegabyte(bytes) {
971
+ return Math.ceil(bytes / 1024 / 1024);
972
+ }
973
+ get middlewares() {
974
+ return [offset(this.offsetValue), shift({
975
+ padding: this.paddingValue
976
+ }), flip()];
977
+ }
978
+ get shouldOpenOnMount() {
979
+ return localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened' && !this.onMobile;
980
+ }
981
+ get shouldAutofocusCompose() {
982
+ return !this.usesVirtualKeyboard;
983
+ }
984
+ get usesVirtualKeyboard() {
985
+ if (typeof navigator === 'undefined') return false;
986
+ const userAgent = navigator.userAgent || '';
987
+ const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
988
+ const isMobileUserAgent = MOBILE_USER_AGENT_PATTERN.test(userAgent);
989
+ const isUserAgentDataMobile = navigator.userAgentData?.mobile === true;
990
+ return isMobileUserAgent || isIPadOS || isUserAgentDataMobile || this.hasTouchOnlyPointer;
991
+ }
992
+ get hasTouchOnlyPointer() {
993
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
994
+ return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(hover: none)').matches;
995
+ }
996
+ get onMobile() {
997
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
998
+ return window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches;
999
+ }
1000
+ }