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