@hellotext/hellotext 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,9 +40,8 @@ var _default = /*#__PURE__*/function (_Controller) {
40
40
  this.webChatChannel = new WebchatChannel(this.idValue, Hellotext.session, this.conversationIdValue);
41
41
  this.files = [];
42
42
  this.onMessageReceived = this.onMessageReceived.bind(this);
43
- this.onConversationAssignment = this.onConversationAssignment.bind(this);
44
- this.onAgentOnline = this.onAgentOnline.bind(this);
45
43
  this.onMessageReaction = this.onMessageReaction.bind(this);
44
+ this.onTypingStart = this.onTypingStart.bind(this);
46
45
  this.onScroll = this.onScroll.bind(this);
47
46
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
48
47
  this.broadcastChannel = new BroadcastChannel("hellotext--webchat--".concat(this.idValue));
@@ -59,8 +58,7 @@ var _default = /*#__PURE__*/function (_Controller) {
59
58
  popover: this.popoverTarget
60
59
  });
61
60
  this.webChatChannel.onMessage(this.onMessageReceived);
62
- this.webChatChannel.onConversationAssignment(this.onConversationAssignment);
63
- this.webChatChannel.onAgentOnline(this.onAgentOnline);
61
+ this.webChatChannel.onTypingStart(this.onTypingStart);
64
62
  this.webChatChannel.onReaction(this.onMessageReaction);
65
63
  this.messagesContainerTarget.addEventListener('scroll', this.onScroll);
66
64
  if (!Hellotext.business.features.white_label) {
@@ -78,10 +76,85 @@ var _default = /*#__PURE__*/function (_Controller) {
78
76
  value: function disconnect() {
79
77
  this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
80
78
  this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
79
+
80
+ // Clean up typing indicator timeouts
81
+ this.clearTypingIndicator();
81
82
  this.broadcastChannel.close();
82
83
  this.floatingUICleanup();
83
84
  _get(_getPrototypeOf(_default.prototype), "disconnect", this).call(this);
84
85
  }
86
+ }, {
87
+ key: "onTypingStart",
88
+ value: function onTypingStart() {
89
+ if (this.typingIndicatorVisible) {
90
+ return this.resetTypingIndicatorTimer();
91
+ }
92
+ this.showTypingIndicator();
93
+ }
94
+ }, {
95
+ key: "showOptimisticTypingIndicator",
96
+ value: function showOptimisticTypingIndicator() {
97
+ if (this.typingIndicatorVisible) return;
98
+ this.showTypingIndicator();
99
+ }
100
+ }, {
101
+ key: "showTypingIndicator",
102
+ value: function showTypingIndicator() {
103
+ this.clearTypingIndicator();
104
+ this.typingIndicatorVisible = true;
105
+ var indicator = this.typingIndicatorTemplateTarget.cloneNode(true);
106
+ indicator.setAttribute('data-hellotext--webchat-target', 'typingIndicator');
107
+ indicator.style.display = 'flex';
108
+ this.messagesContainerTarget.appendChild(indicator);
109
+ requestAnimationFrame(() => {
110
+ this.messagesContainerTarget.scroll({
111
+ top: this.messagesContainerTarget.scrollHeight,
112
+ behavior: 'instant'
113
+ });
114
+ });
115
+ var timeout = this.typingIndicatorKeepAliveValue;
116
+ this.incomingTypingIndicatorTimeout = setTimeout(() => {
117
+ this.clearTypingIndicator();
118
+ }, timeout);
119
+ }
120
+ }, {
121
+ key: "resetTypingIndicatorTimer",
122
+ value: function resetTypingIndicatorTimer() {
123
+ if (!this.typingIndicatorVisible) return;
124
+
125
+ // Clear existing timeout
126
+ clearTimeout(this.incomingTypingIndicatorTimeout);
127
+ clearTimeout(this.optimisticTypingTimeout);
128
+
129
+ // Use unified timeout for all typing indicators
130
+ var timeout = this.typingIndicatorKeepAliveValue;
131
+ this.incomingTypingIndicatorTimeout = setTimeout(() => {
132
+ this.clearTypingIndicator();
133
+ }, timeout);
134
+ }
135
+ }, {
136
+ key: "clearTypingIndicator",
137
+ value: function clearTypingIndicator() {
138
+ if (this.hasTypingIndicatorTarget) {
139
+ this.typingIndicatorTarget.remove();
140
+ }
141
+ this.typingIndicatorVisible = false;
142
+ clearTimeout(this.incomingTypingIndicatorTimeout);
143
+ clearTimeout(this.optimisticTypingTimeout);
144
+ }
145
+ }, {
146
+ key: "onMessageInputChange",
147
+ value: function onMessageInputChange() {
148
+ this.resizeInput();
149
+ clearTimeout(this.typingIndicatorTimeout);
150
+ if (!this.hasSentTypingIndicator) {
151
+ this.webChatChannel.startTypingIndicator();
152
+ this.hasSentTypingIndicator = true;
153
+ }
154
+ this.typingIndicatorTimeout = setTimeout(() => {
155
+ this.hasSentTypingIndicator = false;
156
+ }, 3000);
157
+ }
85
158
  }, {
86
159
  key: "onOutboundMessageSent",
87
160
  value: function onOutboundMessageSent(event) {
@@ -91,7 +164,13 @@ var _default = /*#__PURE__*/function (_Controller) {
91
164
  var callbacks = {
92
165
  'message:sent': data => {
93
166
  var element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
94
- this.messagesContainerTarget.appendChild(element);
167
+
168
+ // Insert message before typing indicator if one exists
169
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
170
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
171
+ } else {
172
+ this.messagesContainerTarget.appendChild(element);
173
+ }
95
174
  element.scrollIntoView({
96
175
  behavior: 'instant'
97
176
  });
@@ -251,6 +330,11 @@ var _default = /*#__PURE__*/function (_Controller) {
251
330
  element.querySelector('[data-attachment-container]').appendChild(image);
252
331
  });
253
332
  }
333
+
334
+ // Clear any typing indicator when message is received
335
+ if (this.typingIndicatorVisible) {
336
+ this.clearTypingIndicator();
337
+ }
254
338
  this.messagesContainerTarget.appendChild(element);
255
339
  Hellotext.eventEmitter.dispatch('webchat:message:received', _objectSpread(_objectSpread({}, message), {}, {
256
340
  body: element.querySelector('[data-body]').innerText
@@ -258,7 +342,6 @@ var _default = /*#__PURE__*/function (_Controller) {
258
342
  element.scrollIntoView({
259
343
  behavior: 'smooth'
260
344
  });
261
- this.setOfflineTimeout();
262
345
  if (this.openValue) {
263
346
  this.messagesAPI.markAsSeen(id);
264
347
  return;
@@ -268,34 +351,31 @@ var _default = /*#__PURE__*/function (_Controller) {
268
351
  this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount;
269
352
  }
270
353
  }, {
271
- key: "onConversationAssignment",
272
- value: function onConversationAssignment(conversation) {
273
- var {
274
- to: user
275
- } = conversation;
276
- this.titleTarget.innerText = user.name;
277
- if (user.online) {
278
- this.onlineStatusTarget.style.display = 'flex';
279
- } else {
280
- this.onlineStatusTarget.style.display = 'none';
281
- }
282
- }
283
- }, {
284
- key: "onAgentOnline",
285
- value: function onAgentOnline() {
286
- this.onlineStatusTarget.style.display = 'flex';
287
- this.setOfflineTimeout();
354
+ key: "resizeInput",
355
+ value: function resizeInput() {
356
+ var maxHeight = 96;
357
+
358
+ // Temporarily reset height to its natural content size
359
+ this.inputTarget.style.height = 'auto';
360
+
361
+ // Set the height to the scrollHeight, which is the minimum height
362
+ // the element needs to fit its content without a scrollbar.
363
+ var scrollHeight = this.inputTarget.scrollHeight;
364
+ this.inputTarget.style.height = "".concat(Math.min(scrollHeight, maxHeight), "px");
288
365
  }
289
366
  }, {
290
367
  key: "sendMessage",
291
368
  value: function () {
292
- var _sendMessage = _asyncToGenerator(function* () {
369
+ var _sendMessage = _asyncToGenerator(function* (e) {
293
370
  var formData = new FormData();
294
371
  var message = {
295
372
  body: this.inputTarget.value,
296
373
  attachments: this.files
297
374
  };
298
375
  if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
376
+ if (e && e.target) {
377
+ e.preventDefault();
378
+ }
299
379
  return;
300
380
  }
301
381
  if (this.inputTarget.value.trim().length > 0) {
@@ -324,7 +404,13 @@ var _default = /*#__PURE__*/function (_Controller) {
324
404
  element.querySelector('[data-attachment-container]').appendChild(attachment);
325
405
  });
326
406
  }
327
- this.messagesContainerTarget.appendChild(element);
407
+
408
+ // Insert message before typing indicator if one exists
409
+ if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
410
+ this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget);
411
+ } else {
412
+ this.messagesContainerTarget.appendChild(element);
413
+ }
328
414
  element.scrollIntoView({
329
415
  behavior: 'smooth'
330
416
  });
@@ -333,6 +419,7 @@ var _default = /*#__PURE__*/function (_Controller) {
333
419
  element: element.outerHTML
334
420
  });
335
421
  this.inputTarget.value = '';
422
+ this.resizeInput();
336
423
  this.files = [];
337
424
  this.attachmentInputTarget.value = '';
338
425
  this.attachmentContainerTarget.innerHTML = '';
@@ -355,9 +442,17 @@ var _default = /*#__PURE__*/function (_Controller) {
355
442
  this.conversationIdValue = data.conversation;
356
443
  this.webChatChannel.updateSubscriptionWith(this.conversationIdValue);
357
444
  }
445
+ if (this.typingIndicatorVisible) {
446
+ this.resetTypingIndicatorTimer();
447
+ } else {
448
+ clearTimeout(this.optimisticTypingTimeout);
449
+ this.optimisticTypingTimeout = setTimeout(() => {
450
+ this.showOptimisticTypingIndicator();
451
+ }, this.optimisticTypingIndicatorWaitValue);
452
+ }
358
453
  this.attachmentContainerTarget.style.display = '';
359
454
  });
360
- function sendMessage() {
455
+ function sendMessage(_x) {
361
456
  return _sendMessage.apply(this, arguments);
362
457
  }
363
458
  return sendMessage;
@@ -458,29 +553,16 @@ var _default = /*#__PURE__*/function (_Controller) {
458
553
  this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length;
459
554
  this.inputTarget.focus();
460
555
  }
461
- }, {
462
- key: "setOfflineTimeout",
463
- value: function setOfflineTimeout() {
464
- clearTimeout(this.offlineTimeout);
465
- this.offlineTimeout = setTimeout(() => {
466
- this.onlineStatusTarget.style.display = 'none';
467
- }, this.fiveMinutes);
468
- }
469
556
  }, {
470
557
  key: "byteToMegabyte",
471
558
  value: function byteToMegabyte(bytes) {
472
559
  return Math.ceil(bytes / 1024 / 1024);
473
560
  }
474
- }, {
475
- key: "fiveMinutes",
476
- get: function get() {
477
- return 300000;
478
- }
479
561
  }, {
480
562
  key: "middlewares",
481
563
  get: function get() {
482
- return [offset(5), shift({
483
- padding: 24
564
+ return [offset(this.offsetValue), shift({
565
+ padding: this.paddingValue
484
566
  }), flip()];
485
567
  }
486
568
  }, {
@@ -524,8 +606,25 @@ _default.values = {
524
606
  fullScreenThreshold: {
525
607
  type: Number,
526
608
  default: 1024
527
- }
609
+ },
610
+ typingIndicatorKeepAlive: {
611
+ type: Number,
612
+ default: 30000
613
+ },
614
+ // 30 seconds
615
+ offset: {
616
+ type: Number,
617
+ default: 24
618
+ },
619
+ padding: {
620
+ type: Number,
621
+ default: 24
622
+ },
623
+ optimisticTypingIndicatorWait: {
624
+ type: Number,
625
+ default: 1000
626
+ } // 1 second
528
627
  };
529
628
  _default.classes = ['fadeOut'];
530
- _default.targets = ['trigger', 'popover', 'input', 'attachmentInput', 'attachmentButton', 'errorMessageContainer', 'attachmentTemplate', 'attachmentContainer', 'attachment', 'messageTemplate', 'messagesContainer', 'title', 'onlineStatus', 'attachmentImage', 'footer', 'toolbar', 'message', 'unreadCounter'];
629
+ _default.targets = ['trigger', 'popover', 'input', 'attachmentInput', 'attachmentButton', 'errorMessageContainer', 'attachmentTemplate', 'attachmentContainer', 'attachment', 'messageTemplate', 'messagesContainer', 'title', 'attachmentImage', 'footer', 'toolbar', 'message', 'unreadCounter', 'typingIndicator', 'typingIndicatorTemplate'];
531
630
  export { _default as default };
@@ -69,6 +69,7 @@ let Configuration = /*#__PURE__*/function () {
69
69
  }();
70
70
  exports.Configuration = Configuration;
71
71
  Configuration.apiRoot = 'https://api.hellotext.com/v1';
72
+ Configuration.actionCableUrl = 'wss://www.hellotext.com/cable';
72
73
  Configuration.autoGenerateSession = true;
73
74
  Configuration.session = null;
74
75
  Configuration.forms = _forms.Forms;
@@ -64,6 +64,7 @@ var Configuration = /*#__PURE__*/function () {
64
64
  return Configuration;
65
65
  }();
66
66
  Configuration.apiRoot = 'https://api.hellotext.com/v1';
67
+ Configuration.actionCableUrl = 'wss://www.hellotext.com/cable';
67
68
  Configuration.autoGenerateSession = true;
68
69
  Configuration.session = null;
69
70
  Configuration.forms = Forms;
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.Query = void 0;
7
- var _cookies = require("./cookies");
8
7
  function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
9
8
  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); } }
10
9
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
@@ -33,7 +32,7 @@ let Query = /*#__PURE__*/function () {
33
32
  }, {
34
33
  key: "session",
35
34
  get: function () {
36
- return this.get('session') || _cookies.Cookies.get('hello_session');
35
+ return this.get('session');
37
36
  }
38
37
  }, {
39
38
  key: "toHellotextParam",
@@ -3,7 +3,6 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
3
3
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
4
4
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
5
5
  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); }
6
- import { Cookies } from './cookies';
7
6
  var Query = /*#__PURE__*/function () {
8
7
  function Query() {
9
8
  _classCallCheck(this, Query);
@@ -27,7 +26,7 @@ var Query = /*#__PURE__*/function () {
27
26
  }, {
28
27
  key: "session",
29
28
  get: function get() {
30
- return this.get('session') || Cookies.get('hello_session');
29
+ return this.get('session');
31
30
  }
32
31
  }, {
33
32
  key: "toHellotextParam",
@@ -34,7 +34,7 @@ let Session = /*#__PURE__*/function () {
34
34
  key: "initialize",
35
35
  value: function initialize() {
36
36
  _classPrivateFieldLooseBase(this, _query)[_query] = new _query2.Query();
37
- this.session = _core.Configuration.session || _classPrivateFieldLooseBase(this, _query)[_query].session || _cookies.Cookies.get('hello_session');
37
+ this.session = _classPrivateFieldLooseBase(this, _query)[_query].session || _core.Configuration.session || _cookies.Cookies.get('hello_session');
38
38
  if (!this.session && _core.Configuration.autoGenerateSession) {
39
39
  this.session = crypto.randomUUID();
40
40
  }
@@ -28,7 +28,7 @@ var Session = /*#__PURE__*/function () {
28
28
  key: "initialize",
29
29
  value: function initialize() {
30
30
  _classPrivateFieldLooseBase(this, _query)[_query] = new Query();
31
- this.session = Configuration.session || _classPrivateFieldLooseBase(this, _query)[_query].session || Cookies.get('hello_session');
31
+ this.session = _classPrivateFieldLooseBase(this, _query)[_query].session || Configuration.session || Cookies.get('hello_session');
32
32
  if (!this.session && Configuration.autoGenerateSession) {
33
33
  this.session = crypto.randomUUID();
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -1,28 +1,31 @@
1
+ import { Configuration } from '../core'
2
+
1
3
  class ApplicationChannel {
2
- static webSocket;
4
+ static webSocket
3
5
 
4
- send({ command, identifier }) {
5
- const data = {
6
+ send({ command, identifier, data }) {
7
+ const payload = {
6
8
  command,
7
- identifier: JSON.stringify(identifier)
9
+ identifier: JSON.stringify(identifier),
10
+ data: JSON.stringify(data || {}),
8
11
  }
9
12
 
10
13
  if (this.webSocket.readyState === WebSocket.OPEN) {
11
- this.webSocket.send(JSON.stringify(data))
14
+ this.webSocket.send(JSON.stringify(payload))
12
15
  } else {
13
16
  this.webSocket.addEventListener('open', () => {
14
- this.webSocket.send(JSON.stringify(data))
17
+ this.webSocket.send(JSON.stringify(payload))
15
18
  })
16
19
  }
17
20
  }
18
21
 
19
22
  onMessage(callback) {
20
- this.webSocket.addEventListener('message', (event) => {
23
+ this.webSocket.addEventListener('message', event => {
21
24
  const data = JSON.parse(event.data)
22
25
  const { type, message } = data
23
26
 
24
27
  if (this.ignoredEvents.includes(type)) {
25
- return;
28
+ return
26
29
  }
27
30
 
28
31
  callback(message)
@@ -31,7 +34,7 @@ class ApplicationChannel {
31
34
 
32
35
  get webSocket() {
33
36
  if (!ApplicationChannel.webSocket) {
34
- return ApplicationChannel.webSocket = new WebSocket("wss://www.hellotext.com/cable")
37
+ return (ApplicationChannel.webSocket = new WebSocket(Configuration.actionCableUrl))
35
38
  }
36
39
 
37
40
  return ApplicationChannel.webSocket
@@ -13,18 +13,18 @@ class WebchatChannel extends ApplicationChannel {
13
13
 
14
14
  subscribe() {
15
15
  const params = {
16
- channel: "WebchatChannel",
16
+ channel: 'WebchatChannel',
17
17
  id: this.id,
18
18
  session: this.session,
19
19
  conversation: this.conversation,
20
20
  }
21
21
 
22
- this.send( { command: 'subscribe', identifier: params })
22
+ this.send({ command: 'subscribe', identifier: params })
23
23
  }
24
24
 
25
25
  unsubscribe() {
26
26
  const params = {
27
- channel: "WebchatChannel",
27
+ channel: 'WebchatChannel',
28
28
  id: this.id,
29
29
  session: this.session,
30
30
  conversation: this.conversation,
@@ -33,32 +33,46 @@ class WebchatChannel extends ApplicationChannel {
33
33
  this.send({ command: 'unsubscribe', identifier: params })
34
34
  }
35
35
 
36
- onMessage(callback) {
37
- super.onMessage((message) => {
38
- if(message.type !== 'message') return
39
- callback(message)
40
- })
36
+ startTypingIndicator() {
37
+ const params = {
38
+ channel: 'WebchatChannel',
39
+ id: this.id,
40
+ session: this.session,
41
+ conversation: this.conversation,
42
+ }
43
+
44
+ this.send({ command: 'message', identifier: params, data: { action: 'started_typing' } })
41
45
  }
42
46
 
43
- onConversationAssignment(callback) {
44
- super.onMessage((message) => {
45
- if(message.type === 'conversation.assigned') {
46
- callback(message)
47
- }
47
+ stopTypingIndicator() {
48
+ const params = {
49
+ channel: 'WebchatChannel',
50
+ id: this.id,
51
+ session: this.session,
52
+ conversation: this.conversation,
53
+ }
54
+
55
+ this.send({ command: 'typing:stop', identifier: params, data: { action: 'stopped_typing' } })
56
+ }
57
+
58
+ onMessage(callback) {
59
+ super.onMessage(message => {
60
+ if (message.type !== 'message') return
61
+ callback(message)
48
62
  })
49
63
  }
50
64
 
51
- onAgentOnline(callback) {
52
- super.onMessage((message) => {
53
- if(message.type === 'agent_is_online') {
65
+ onReaction(callback) {
66
+ super.onMessage(message => {
67
+ if (message.type === 'reaction.create' || message.type === 'reaction.destroy') {
54
68
  callback(message)
55
69
  }
56
70
  })
57
71
  }
58
72
 
59
- onReaction(callback) {
60
- super.onMessage((message) => {
61
- if(message.type === 'reaction.create' || message.type === 'reaction.destroy') {
73
+ onTypingStart(callback) {
74
+ super.onMessage(message => {
75
+ if (message.type === 'started_typing') {
62
76
  callback(message)
63
77
  }
64
78
  })