@hellotext/hellotext 2.4.4 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _dom = require("@floating-ui/dom");
8
8
  var _stimulus = require("@hotwired/stimulus");
9
- var _messages = _interopRequireDefault(require("../api/webchat/messages"));
9
+ var _messages2 = _interopRequireDefault(require("../api/webchat/messages"));
10
10
  var _webchat_channel = _interopRequireDefault(require("../channels/webchat_channel"));
11
11
  var _hellotext = _interopRequireDefault(require("../hellotext"));
12
12
  var _core = require("../core");
@@ -35,6 +35,7 @@ const MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
35
35
  hour: 'numeric',
36
36
  minute: '2-digit'
37
37
  };
38
+ const MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
38
39
  const SCROLL_ISOLATION_EVENT_OPTIONS = {
39
40
  capture: true,
40
41
  passive: true
@@ -49,17 +50,23 @@ let _default = /*#__PURE__*/function (_Controller) {
49
50
  _createClass(_default, [{
50
51
  key: "initialize",
51
52
  value: function initialize() {
52
- this.messagesAPI = new _messages.default(this.idValue);
53
+ this.messagesAPI = new _messages2.default(this.idValue);
53
54
  this.webChatChannel = new _webchat_channel.default(this.idValue, _hellotext.default.session, this.conversationIdValue);
54
55
  this.files = [];
55
56
  this.messageIds = new Set();
57
+ this.catchUpAfterMessageId = null;
58
+ this.fetchingCatchUpMessages = false;
56
59
  this.onMessageReceived = this.onMessageReceived.bind(this);
57
60
  this.onMessageReaction = this.onMessageReaction.bind(this);
58
61
  this.onTypingStart = this.onTypingStart.bind(this);
62
+ this.captureCatchUpCursor = this.captureCatchUpCursor.bind(this);
63
+ this.catchUpMessages = this.catchUpMessages.bind(this);
59
64
  this.onScroll = this.onScroll.bind(this);
60
65
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
61
66
  this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
62
67
  this.broadcastChannel = new BroadcastChannel(`hellotext--webchat--${this.idValue}`);
68
+ this.webChatChannel.onDisconnect(this.captureCatchUpCursor);
69
+ this.webChatChannel.onReconnect(this.catchUpMessages);
63
70
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
64
71
  }
65
72
  }, {
@@ -261,7 +268,12 @@ let _default = /*#__PURE__*/function (_Controller) {
261
268
  const div = document.createElement('div');
262
269
  div.innerHTML = body;
263
270
  const element = this.messageTemplateTarget.cloneNode(true);
271
+ element.classList.add('hellotext--webchat-message');
264
272
  element.setAttribute('data-hellotext--webchat-target', 'message');
273
+ element.setAttribute('data-id', message.id);
274
+ if (createdAt) {
275
+ element.setAttribute('data-created-at', createdAt);
276
+ }
265
277
  element.style.removeProperty('display');
266
278
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
267
279
  if (message.state === 'received') {
@@ -331,7 +343,7 @@ let _default = /*#__PURE__*/function (_Controller) {
331
343
  this.popoverTarget.classList.remove(...this.fadeOutClasses);
332
344
  (_this$dismissTeaserFo = this.dismissTeaserForSession) === null || _this$dismissTeaserFo === void 0 ? void 0 : _this$dismissTeaserFo.call(this);
333
345
  if (!this.onMobile) {
334
- this.inputTarget.focus();
346
+ this.focusComposeInput();
335
347
  }
336
348
  if (!this.scrolled) {
337
349
  requestAnimationFrame(() => {
@@ -386,7 +398,7 @@ let _default = /*#__PURE__*/function (_Controller) {
386
398
  }
387
399
  }, {
388
400
  key: "onMessageReceived",
389
- value: function onMessageReceived(message) {
401
+ value: function onMessageReceived(message, options = {}) {
390
402
  var _this$hideTeaser;
391
403
  const {
392
404
  id,
@@ -398,15 +410,17 @@ let _default = /*#__PURE__*/function (_Controller) {
398
410
  if (!this.claimMessageId(id)) return;
399
411
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
400
412
  if (message.carousel) {
401
- return this.insertCarouselMessage(message);
413
+ return this.insertCarouselMessage(message, options);
402
414
  }
403
415
  const div = document.createElement('div');
404
416
  div.innerHTML = body;
405
417
  const element = this.messageTemplateTarget.cloneNode(true);
418
+ element.classList.add('hellotext--webchat-message');
406
419
  element.style.display = 'flex';
407
420
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
408
421
  element.setAttribute('data-id', id);
409
422
  element.setAttribute('data-hellotext--webchat-target', 'message');
423
+ this.setMessageCreatedAt(element, createdAt);
410
424
  this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
411
425
  if (attachments) {
412
426
  attachments.forEach(attachmentUrl => {
@@ -418,14 +432,16 @@ let _default = /*#__PURE__*/function (_Controller) {
418
432
  });
419
433
  }
420
434
  this.clearTypingIndicator();
421
- this.messagesContainerTarget.appendChild(element);
435
+ this.insertMessageElement(element);
422
436
  _hellotext.default.eventEmitter.dispatch('webchat:message:received', {
423
437
  ...message,
424
438
  body: element.querySelector('[data-body]').innerText
425
439
  });
426
- element.scrollIntoView({
427
- behavior: 'smooth'
428
- });
440
+ if (options.scroll !== false) {
441
+ element.scrollIntoView({
442
+ behavior: 'smooth'
443
+ });
444
+ }
429
445
  this.updateMessageTeaser(teaser);
430
446
  if (this.openValue) {
431
447
  this.messagesAPI.markAsSeen(id);
@@ -445,6 +461,70 @@ let _default = /*#__PURE__*/function (_Controller) {
445
461
  this.messageIds.add(id);
446
462
  return !messageTargets.some(element => element.dataset.id === id);
447
463
  }
464
+ }, {
465
+ key: "captureCatchUpCursor",
466
+ value: function captureCatchUpCursor() {
467
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
468
+ }
469
+ }, {
470
+ key: "catchUpMessages",
471
+ value: async function catchUpMessages() {
472
+ const afterId = this.catchUpAfterMessageId;
473
+ if (!afterId || this.fetchingCatchUpMessages) return;
474
+ this.fetchingCatchUpMessages = true;
475
+ try {
476
+ const response = await this.messagesAPI.catchUp(afterId);
477
+ const {
478
+ messages = []
479
+ } = await response.json();
480
+ messages.forEach(message => this.onMessageReceived(message, {
481
+ scroll: false
482
+ }));
483
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
484
+ } finally {
485
+ this.fetchingCatchUpMessages = false;
486
+ }
487
+ }
488
+ }, {
489
+ key: "lastRenderedMessageId",
490
+ get: function () {
491
+ var _messages;
492
+ const messages = this.persistedMessageElements;
493
+ return ((_messages = messages[messages.length - 1]) === null || _messages === void 0 ? void 0 : _messages.dataset.id) || null;
494
+ }
495
+ }, {
496
+ key: "persistedMessageElements",
497
+ get: function () {
498
+ return Array.from(this.messagesContainerTarget.querySelectorAll('.hellotext--webchat-message[data-id]'));
499
+ }
500
+ }, {
501
+ key: "setMessageCreatedAt",
502
+ value: function setMessageCreatedAt(element, createdAt) {
503
+ if (createdAt) {
504
+ element.setAttribute('data-created-at', createdAt);
505
+ }
506
+ }
507
+ }, {
508
+ key: "insertMessageElement",
509
+ value: function insertMessageElement(element) {
510
+ const nextElement = this.nextMessageElementFor(element);
511
+ if (nextElement) {
512
+ this.messagesContainerTarget.insertBefore(element, nextElement);
513
+ } else {
514
+ this.messagesContainerTarget.appendChild(element);
515
+ }
516
+ }
517
+ }, {
518
+ key: "nextMessageElementFor",
519
+ value: function nextMessageElementFor(element) {
520
+ const createdAt = Date.parse(element.dataset.createdAt);
521
+ if (Number.isNaN(createdAt)) return null;
522
+ return this.persistedMessageElements.find(messageElement => {
523
+ if (messageElement === element) return false;
524
+ const messageCreatedAt = Date.parse(messageElement.dataset.createdAt);
525
+ return !Number.isNaN(messageCreatedAt) && messageCreatedAt > createdAt;
526
+ });
527
+ }
448
528
  }, {
449
529
  key: "updateMessageTeaser",
450
530
  value: function updateMessageTeaser(teaser) {
@@ -459,18 +539,23 @@ let _default = /*#__PURE__*/function (_Controller) {
459
539
  }
460
540
  }, {
461
541
  key: "insertCarouselMessage",
462
- value: function insertCarouselMessage(message) {
542
+ value: function insertCarouselMessage(message, options = {}) {
463
543
  var _element$querySelecto;
464
544
  const html = message.html;
545
+ const createdAt = message.created_at || message.createdAt;
465
546
  const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
547
+ element.classList.add('hellotext--webchat-message');
466
548
  element.setAttribute('data-id', message.id);
467
549
  element.setAttribute('data-hellotext--webchat-target', 'message');
550
+ this.setMessageCreatedAt(element, createdAt);
468
551
  this.localizeMessageTimestamps(element);
469
552
  this.clearTypingIndicator();
470
- this.messagesContainerTarget.appendChild(element);
471
- element.scrollIntoView({
472
- behavior: 'smooth'
473
- });
553
+ this.insertMessageElement(element);
554
+ if (options.scroll !== false) {
555
+ element.scrollIntoView({
556
+ behavior: 'smooth'
557
+ });
558
+ }
474
559
  _hellotext.default.eventEmitter.dispatch('webchat:message:received', {
475
560
  ...message,
476
561
  body: ((_element$querySelecto = element.querySelector('[data-body]')) === null || _element$querySelecto === void 0 ? void 0 : _element$querySelecto.innerText) || ''
@@ -689,7 +774,7 @@ let _default = /*#__PURE__*/function (_Controller) {
689
774
  this.attachmentContainerTarget.innerHTML = '';
690
775
  this.attachmentContainerTarget.style.display = 'none';
691
776
  this.errorMessageContainerTarget.style.display = 'none';
692
- this.inputTarget.focus();
777
+ this.focusComposeInput();
693
778
 
694
779
  // Set up optimistic typing indicator BEFORE making the API call
695
780
  // This prevents race conditions with server responses
@@ -742,12 +827,10 @@ let _default = /*#__PURE__*/function (_Controller) {
742
827
  } = event;
743
828
  const ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
744
829
  if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
830
+ if (!this.focusComposeInput({
831
+ moveCursorToEnd: true
832
+ })) return;
745
833
  event.preventDefault();
746
- this.inputTarget.focus();
747
- if (typeof this.inputTarget.selectionStart === 'number') {
748
- const position = this.inputTarget.value.length;
749
- this.inputTarget.setSelectionRange(position, position);
750
- }
751
834
  }
752
835
  }, {
753
836
  key: "closePopoverFromHeader",
@@ -896,7 +979,7 @@ let _default = /*#__PURE__*/function (_Controller) {
896
979
  this.files = [...this.files, ...newFiles];
897
980
  this.errorMessageContainerTarget.innerText = '';
898
981
  newFiles.forEach(file => this.createAttachmentElement(file));
899
- this.inputTarget.focus();
982
+ this.focusComposeInput();
900
983
  }
901
984
  }, {
902
985
  key: "createAttachmentElement",
@@ -931,7 +1014,7 @@ let _default = /*#__PURE__*/function (_Controller) {
931
1014
  this.files = this.files.filter(file => file.name !== attachment.dataset.name);
932
1015
  this.attachmentInputTarget.value = '';
933
1016
  attachment.remove();
934
- this.inputTarget.focus();
1017
+ this.focusComposeInput();
935
1018
  }
936
1019
  }, {
937
1020
  key: "attachmentTargetDisconnected",
@@ -960,7 +1043,22 @@ let _default = /*#__PURE__*/function (_Controller) {
960
1043
  const end = this.inputTarget.selectionEnd;
961
1044
  this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end);
962
1045
  this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
1046
+ this.focusComposeInput();
1047
+ }
1048
+ }, {
1049
+ key: "focusComposeInput",
1050
+ value: function focusComposeInput({
1051
+ moveCursorToEnd = false
1052
+ } = {}) {
1053
+ if (!this.shouldAutofocusCompose) return false;
1054
+ if (this.hasInputTarget === false) return false;
1055
+ if (this.hasInputTarget === undefined && !this.inputTarget) return false;
963
1056
  this.inputTarget.focus();
1057
+ if (moveCursorToEnd && typeof this.inputTarget.selectionStart === 'number') {
1058
+ const position = this.inputTarget.value.length;
1059
+ this.inputTarget.setSelectionRange(position, position);
1060
+ }
1061
+ return true;
964
1062
  }
965
1063
  }, {
966
1064
  key: "byteToMegabyte",
@@ -979,9 +1077,32 @@ let _default = /*#__PURE__*/function (_Controller) {
979
1077
  get: function () {
980
1078
  return localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened' && !this.onMobile;
981
1079
  }
1080
+ }, {
1081
+ key: "shouldAutofocusCompose",
1082
+ get: function () {
1083
+ return !this.usesVirtualKeyboard;
1084
+ }
1085
+ }, {
1086
+ key: "usesVirtualKeyboard",
1087
+ get: function () {
1088
+ var _navigator$userAgentD;
1089
+ if (typeof navigator === 'undefined') return false;
1090
+ const userAgent = navigator.userAgent || '';
1091
+ const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
1092
+ const isMobileUserAgent = MOBILE_USER_AGENT_PATTERN.test(userAgent);
1093
+ const isUserAgentDataMobile = ((_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.mobile) === true;
1094
+ return isMobileUserAgent || isIPadOS || isUserAgentDataMobile || this.hasTouchOnlyPointer;
1095
+ }
1096
+ }, {
1097
+ key: "hasTouchOnlyPointer",
1098
+ get: function () {
1099
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1100
+ return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(hover: none)').matches;
1101
+ }
982
1102
  }, {
983
1103
  key: "onMobile",
984
1104
  get: function () {
1105
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
985
1106
  return window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches;
986
1107
  }
987
1108
  }], [{
@@ -33,6 +33,7 @@ var MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
33
33
  hour: 'numeric',
34
34
  minute: '2-digit'
35
35
  };
36
+ var MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
36
37
  var SCROLL_ISOLATION_EVENT_OPTIONS = {
37
38
  capture: true,
38
39
  passive: true
@@ -51,13 +52,19 @@ var _default = /*#__PURE__*/function (_Controller) {
51
52
  this.webChatChannel = new WebchatChannel(this.idValue, Hellotext.session, this.conversationIdValue);
52
53
  this.files = [];
53
54
  this.messageIds = new Set();
55
+ this.catchUpAfterMessageId = null;
56
+ this.fetchingCatchUpMessages = false;
54
57
  this.onMessageReceived = this.onMessageReceived.bind(this);
55
58
  this.onMessageReaction = this.onMessageReaction.bind(this);
56
59
  this.onTypingStart = this.onTypingStart.bind(this);
60
+ this.captureCatchUpCursor = this.captureCatchUpCursor.bind(this);
61
+ this.catchUpMessages = this.catchUpMessages.bind(this);
57
62
  this.onScroll = this.onScroll.bind(this);
58
63
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
59
64
  this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
60
65
  this.broadcastChannel = new BroadcastChannel("hellotext--webchat--".concat(this.idValue));
66
+ this.webChatChannel.onDisconnect(this.captureCatchUpCursor);
67
+ this.webChatChannel.onReconnect(this.catchUpMessages);
61
68
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
62
69
  }
63
70
  }, {
@@ -260,7 +267,12 @@ var _default = /*#__PURE__*/function (_Controller) {
260
267
  var div = document.createElement('div');
261
268
  div.innerHTML = body;
262
269
  var element = this.messageTemplateTarget.cloneNode(true);
270
+ element.classList.add('hellotext--webchat-message');
263
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
+ }
264
276
  element.style.removeProperty('display');
265
277
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
266
278
  if (message.state === 'received') {
@@ -335,7 +347,7 @@ var _default = /*#__PURE__*/function (_Controller) {
335
347
  this.popoverTarget.classList.remove(...this.fadeOutClasses);
336
348
  (_this$dismissTeaserFo = this.dismissTeaserForSession) === null || _this$dismissTeaserFo === void 0 ? void 0 : _this$dismissTeaserFo.call(this);
337
349
  if (!this.onMobile) {
338
- this.inputTarget.focus();
350
+ this.focusComposeInput();
339
351
  }
340
352
  if (!this.scrolled) {
341
353
  requestAnimationFrame(() => {
@@ -392,6 +404,7 @@ var _default = /*#__PURE__*/function (_Controller) {
392
404
  key: "onMessageReceived",
393
405
  value: function onMessageReceived(message) {
394
406
  var _this$hideTeaser;
407
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
395
408
  var {
396
409
  id,
397
410
  body,
@@ -402,15 +415,17 @@ var _default = /*#__PURE__*/function (_Controller) {
402
415
  if (!this.claimMessageId(id)) return;
403
416
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
404
417
  if (message.carousel) {
405
- return this.insertCarouselMessage(message);
418
+ return this.insertCarouselMessage(message, options);
406
419
  }
407
420
  var div = document.createElement('div');
408
421
  div.innerHTML = body;
409
422
  var element = this.messageTemplateTarget.cloneNode(true);
423
+ element.classList.add('hellotext--webchat-message');
410
424
  element.style.display = 'flex';
411
425
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
412
426
  element.setAttribute('data-id', id);
413
427
  element.setAttribute('data-hellotext--webchat-target', 'message');
428
+ this.setMessageCreatedAt(element, createdAt);
414
429
  this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
415
430
  if (attachments) {
416
431
  attachments.forEach(attachmentUrl => {
@@ -422,13 +437,15 @@ var _default = /*#__PURE__*/function (_Controller) {
422
437
  });
423
438
  }
424
439
  this.clearTypingIndicator();
425
- this.messagesContainerTarget.appendChild(element);
440
+ this.insertMessageElement(element);
426
441
  Hellotext.eventEmitter.dispatch('webchat:message:received', _objectSpread(_objectSpread({}, message), {}, {
427
442
  body: element.querySelector('[data-body]').innerText
428
443
  }));
429
- element.scrollIntoView({
430
- behavior: 'smooth'
431
- });
444
+ if (options.scroll !== false) {
445
+ element.scrollIntoView({
446
+ behavior: 'smooth'
447
+ });
448
+ }
432
449
  this.updateMessageTeaser(teaser);
433
450
  if (this.openValue) {
434
451
  this.messagesAPI.markAsSeen(id);
@@ -448,6 +465,76 @@ var _default = /*#__PURE__*/function (_Controller) {
448
465
  this.messageIds.add(id);
449
466
  return !messageTargets.some(element => element.dataset.id === id);
450
467
  }
468
+ }, {
469
+ key: "captureCatchUpCursor",
470
+ value: function captureCatchUpCursor() {
471
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
472
+ }
473
+ }, {
474
+ key: "catchUpMessages",
475
+ value: function () {
476
+ var _catchUpMessages = _asyncToGenerator(function* () {
477
+ var afterId = this.catchUpAfterMessageId;
478
+ if (!afterId || this.fetchingCatchUpMessages) return;
479
+ this.fetchingCatchUpMessages = true;
480
+ try {
481
+ var response = yield this.messagesAPI.catchUp(afterId);
482
+ var {
483
+ messages = []
484
+ } = yield response.json();
485
+ messages.forEach(message => this.onMessageReceived(message, {
486
+ scroll: false
487
+ }));
488
+ this.catchUpAfterMessageId = this.lastRenderedMessageId;
489
+ } finally {
490
+ this.fetchingCatchUpMessages = false;
491
+ }
492
+ });
493
+ function catchUpMessages() {
494
+ return _catchUpMessages.apply(this, arguments);
495
+ }
496
+ return catchUpMessages;
497
+ }()
498
+ }, {
499
+ key: "lastRenderedMessageId",
500
+ get: function get() {
501
+ var _messages;
502
+ var messages = this.persistedMessageElements;
503
+ return ((_messages = messages[messages.length - 1]) === null || _messages === void 0 ? void 0 : _messages.dataset.id) || null;
504
+ }
505
+ }, {
506
+ key: "persistedMessageElements",
507
+ get: function get() {
508
+ return Array.from(this.messagesContainerTarget.querySelectorAll('.hellotext--webchat-message[data-id]'));
509
+ }
510
+ }, {
511
+ key: "setMessageCreatedAt",
512
+ value: function setMessageCreatedAt(element, createdAt) {
513
+ if (createdAt) {
514
+ element.setAttribute('data-created-at', createdAt);
515
+ }
516
+ }
517
+ }, {
518
+ key: "insertMessageElement",
519
+ value: function insertMessageElement(element) {
520
+ var nextElement = this.nextMessageElementFor(element);
521
+ if (nextElement) {
522
+ this.messagesContainerTarget.insertBefore(element, nextElement);
523
+ } else {
524
+ this.messagesContainerTarget.appendChild(element);
525
+ }
526
+ }
527
+ }, {
528
+ key: "nextMessageElementFor",
529
+ value: function nextMessageElementFor(element) {
530
+ var createdAt = Date.parse(element.dataset.createdAt);
531
+ if (Number.isNaN(createdAt)) return null;
532
+ return this.persistedMessageElements.find(messageElement => {
533
+ if (messageElement === element) return false;
534
+ var messageCreatedAt = Date.parse(messageElement.dataset.createdAt);
535
+ return !Number.isNaN(messageCreatedAt) && messageCreatedAt > createdAt;
536
+ });
537
+ }
451
538
  }, {
452
539
  key: "updateMessageTeaser",
453
540
  value: function updateMessageTeaser(teaser) {
@@ -464,16 +551,22 @@ var _default = /*#__PURE__*/function (_Controller) {
464
551
  key: "insertCarouselMessage",
465
552
  value: function insertCarouselMessage(message) {
466
553
  var _element$querySelecto;
554
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
467
555
  var html = message.html;
556
+ var createdAt = message.created_at || message.createdAt;
468
557
  var element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
558
+ element.classList.add('hellotext--webchat-message');
469
559
  element.setAttribute('data-id', message.id);
470
560
  element.setAttribute('data-hellotext--webchat-target', 'message');
561
+ this.setMessageCreatedAt(element, createdAt);
471
562
  this.localizeMessageTimestamps(element);
472
563
  this.clearTypingIndicator();
473
- this.messagesContainerTarget.appendChild(element);
474
- element.scrollIntoView({
475
- behavior: 'smooth'
476
- });
564
+ this.insertMessageElement(element);
565
+ if (options.scroll !== false) {
566
+ element.scrollIntoView({
567
+ behavior: 'smooth'
568
+ });
569
+ }
477
570
  Hellotext.eventEmitter.dispatch('webchat:message:received', _objectSpread(_objectSpread({}, message), {}, {
478
571
  body: ((_element$querySelecto = element.querySelector('[data-body]')) === null || _element$querySelecto === void 0 ? void 0 : _element$querySelecto.innerText) || ''
479
572
  }));
@@ -705,7 +798,7 @@ var _default = /*#__PURE__*/function (_Controller) {
705
798
  this.attachmentContainerTarget.innerHTML = '';
706
799
  this.attachmentContainerTarget.style.display = 'none';
707
800
  this.errorMessageContainerTarget.style.display = 'none';
708
- this.inputTarget.focus();
801
+ this.focusComposeInput();
709
802
 
710
803
  // Set up optimistic typing indicator BEFORE making the API call
711
804
  // This prevents race conditions with server responses
@@ -763,12 +856,10 @@ var _default = /*#__PURE__*/function (_Controller) {
763
856
  } = event;
764
857
  var ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
765
858
  if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
859
+ if (!this.focusComposeInput({
860
+ moveCursorToEnd: true
861
+ })) return;
766
862
  event.preventDefault();
767
- this.inputTarget.focus();
768
- if (typeof this.inputTarget.selectionStart === 'number') {
769
- var position = this.inputTarget.value.length;
770
- this.inputTarget.setSelectionRange(position, position);
771
- }
772
863
  }
773
864
  }, {
774
865
  key: "closePopoverFromHeader",
@@ -931,7 +1022,7 @@ var _default = /*#__PURE__*/function (_Controller) {
931
1022
  this.files = [...this.files, ...newFiles];
932
1023
  this.errorMessageContainerTarget.innerText = '';
933
1024
  newFiles.forEach(file => this.createAttachmentElement(file));
934
- this.inputTarget.focus();
1025
+ this.focusComposeInput();
935
1026
  }
936
1027
  }, {
937
1028
  key: "createAttachmentElement",
@@ -967,7 +1058,7 @@ var _default = /*#__PURE__*/function (_Controller) {
967
1058
  this.files = this.files.filter(file => file.name !== attachment.dataset.name);
968
1059
  this.attachmentInputTarget.value = '';
969
1060
  attachment.remove();
970
- this.inputTarget.focus();
1061
+ this.focusComposeInput();
971
1062
  }
972
1063
  }, {
973
1064
  key: "attachmentTargetDisconnected",
@@ -997,7 +1088,23 @@ var _default = /*#__PURE__*/function (_Controller) {
997
1088
  var end = this.inputTarget.selectionEnd;
998
1089
  this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end);
999
1090
  this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
1091
+ this.focusComposeInput();
1092
+ }
1093
+ }, {
1094
+ key: "focusComposeInput",
1095
+ value: function focusComposeInput() {
1096
+ var {
1097
+ moveCursorToEnd = false
1098
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1099
+ if (!this.shouldAutofocusCompose) return false;
1100
+ if (this.hasInputTarget === false) return false;
1101
+ if (this.hasInputTarget === undefined && !this.inputTarget) return false;
1000
1102
  this.inputTarget.focus();
1103
+ if (moveCursorToEnd && typeof this.inputTarget.selectionStart === 'number') {
1104
+ var position = this.inputTarget.value.length;
1105
+ this.inputTarget.setSelectionRange(position, position);
1106
+ }
1107
+ return true;
1001
1108
  }
1002
1109
  }, {
1003
1110
  key: "byteToMegabyte",
@@ -1016,9 +1123,32 @@ var _default = /*#__PURE__*/function (_Controller) {
1016
1123
  get: function get() {
1017
1124
  return localStorage.getItem("hellotext--webchat--".concat(this.idValue)) === 'opened' && !this.onMobile;
1018
1125
  }
1126
+ }, {
1127
+ key: "shouldAutofocusCompose",
1128
+ get: function get() {
1129
+ return !this.usesVirtualKeyboard;
1130
+ }
1131
+ }, {
1132
+ key: "usesVirtualKeyboard",
1133
+ get: function get() {
1134
+ var _navigator$userAgentD;
1135
+ if (typeof navigator === 'undefined') return false;
1136
+ var userAgent = navigator.userAgent || '';
1137
+ var isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
1138
+ var isMobileUserAgent = MOBILE_USER_AGENT_PATTERN.test(userAgent);
1139
+ var isUserAgentDataMobile = ((_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.mobile) === true;
1140
+ return isMobileUserAgent || isIPadOS || isUserAgentDataMobile || this.hasTouchOnlyPointer;
1141
+ }
1142
+ }, {
1143
+ key: "hasTouchOnlyPointer",
1144
+ get: function get() {
1145
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1146
+ return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(hover: none)').matches;
1147
+ }
1019
1148
  }, {
1020
1149
  key: "onMobile",
1021
1150
  get: function get() {
1151
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
1022
1152
  return window.matchMedia("(max-width: ".concat(this.fullScreenThresholdValue, "px)")).matches;
1023
1153
  }
1024
1154
  }], [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -25,6 +25,13 @@ class WebchatMessagesAPI {
25
25
  })
26
26
  }
27
27
 
28
+ catchUp(afterId) {
29
+ return this.index({
30
+ after_id: afterId,
31
+ session: Hellotext.session,
32
+ })
33
+ }
34
+
28
35
  async create(formData) {
29
36
  const response = await fetch(this.url, {
30
37
  method: 'POST',