@hellotext/hellotext 2.1.3 → 2.1.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.
- package/dist/hellotext.js +1 -1
- package/lib/channels/application_channel.cjs +9 -6
- package/lib/channels/application_channel.js +9 -6
- package/lib/channels/webchat_channel.cjs +42 -17
- package/lib/channels/webchat_channel.js +42 -17
- package/lib/controllers/webchat_controller.cjs +145 -42
- package/lib/controllers/webchat_controller.js +146 -43
- package/lib/core/configuration.cjs +1 -0
- package/lib/core/configuration.js +1 -0
- package/lib/hellotext.cjs +3 -1
- package/lib/hellotext.js +4 -4
- package/lib/models/index.cjs +14 -0
- package/lib/models/index.js +2 -0
- package/lib/models/page.cjs +86 -0
- package/lib/models/page.js +81 -0
- package/lib/models/query.cjs +1 -2
- package/lib/models/query.js +1 -2
- package/lib/models/session.cjs +1 -1
- package/lib/models/session.js +1 -1
- package/lib/models/utm.cjs +41 -0
- package/lib/models/utm.js +38 -0
- package/package.json +1 -1
- package/src/channels/application_channel.js +12 -9
- package/src/channels/webchat_channel.js +33 -19
- package/src/controllers/webchat_controller.js +137 -34
- package/src/core/configuration.js +1 -0
- package/src/hellotext.js +5 -2
- package/src/models/cookies.js +2 -2
- package/src/models/index.js +2 -0
- package/src/models/page.js +65 -0
- package/src/models/query.js +1 -3
- package/src/models/session.js +1 -1
- package/src/models/utm.js +33 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.UTM = void 0;
|
|
7
|
+
var _cookies = require("./cookies");
|
|
8
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
9
|
+
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
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
11
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
12
|
+
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); }
|
|
13
|
+
let UTM = /*#__PURE__*/function () {
|
|
14
|
+
function UTM() {
|
|
15
|
+
_classCallCheck(this, UTM);
|
|
16
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
17
|
+
const utmsFromUrl = {
|
|
18
|
+
source: urlSearchParams.get('utm_source'),
|
|
19
|
+
medium: urlSearchParams.get('utm_medium'),
|
|
20
|
+
campaign: urlSearchParams.get('utm_campaign'),
|
|
21
|
+
term: urlSearchParams.get('utm_term'),
|
|
22
|
+
content: urlSearchParams.get('utm_content')
|
|
23
|
+
};
|
|
24
|
+
if (utmsFromUrl.source && utmsFromUrl.medium) {
|
|
25
|
+
const cleanUtms = Object.fromEntries(Object.entries(utmsFromUrl).filter(([_, value]) => value));
|
|
26
|
+
_cookies.Cookies.set('hello_utm', JSON.stringify(cleanUtms));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
_createClass(UTM, [{
|
|
30
|
+
key: "current",
|
|
31
|
+
get: function () {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(_cookies.Cookies.get('hello_utm')) || {};
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}]);
|
|
39
|
+
return UTM;
|
|
40
|
+
}();
|
|
41
|
+
exports.UTM = UTM;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
2
|
+
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); } }
|
|
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
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
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
|
+
var UTM = /*#__PURE__*/function () {
|
|
8
|
+
function UTM() {
|
|
9
|
+
_classCallCheck(this, UTM);
|
|
10
|
+
var urlSearchParams = new URLSearchParams(window.location.search);
|
|
11
|
+
var utmsFromUrl = {
|
|
12
|
+
source: urlSearchParams.get('utm_source'),
|
|
13
|
+
medium: urlSearchParams.get('utm_medium'),
|
|
14
|
+
campaign: urlSearchParams.get('utm_campaign'),
|
|
15
|
+
term: urlSearchParams.get('utm_term'),
|
|
16
|
+
content: urlSearchParams.get('utm_content')
|
|
17
|
+
};
|
|
18
|
+
if (utmsFromUrl.source && utmsFromUrl.medium) {
|
|
19
|
+
var cleanUtms = Object.fromEntries(Object.entries(utmsFromUrl).filter(_ref => {
|
|
20
|
+
var [_, value] = _ref;
|
|
21
|
+
return value;
|
|
22
|
+
}));
|
|
23
|
+
Cookies.set('hello_utm', JSON.stringify(cleanUtms));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
_createClass(UTM, [{
|
|
27
|
+
key: "current",
|
|
28
|
+
get: function get() {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(Cookies.get('hello_utm')) || {};
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}]);
|
|
36
|
+
return UTM;
|
|
37
|
+
}();
|
|
38
|
+
export { UTM };
|
package/package.json
CHANGED
|
@@ -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
|
|
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(
|
|
14
|
+
this.webSocket.send(JSON.stringify(payload))
|
|
12
15
|
} else {
|
|
13
16
|
this.webSocket.addEventListener('open', () => {
|
|
14
|
-
this.webSocket.send(JSON.stringify(
|
|
17
|
+
this.webSocket.send(JSON.stringify(payload))
|
|
15
18
|
})
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
onMessage(callback) {
|
|
20
|
-
this.webSocket.addEventListener('message',
|
|
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(
|
|
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:
|
|
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(
|
|
22
|
+
this.send({ command: 'subscribe', identifier: params })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
unsubscribe() {
|
|
26
26
|
const params = {
|
|
27
|
-
channel:
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
super.onMessage(
|
|
53
|
-
if(message.type === '
|
|
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
|
-
|
|
60
|
-
super.onMessage(
|
|
61
|
-
if(message.type === '
|
|
73
|
+
onTypingStart(callback) {
|
|
74
|
+
super.onMessage(message => {
|
|
75
|
+
if (message.type === 'started_typing') {
|
|
62
76
|
callback(message)
|
|
63
77
|
}
|
|
64
78
|
})
|
|
@@ -23,6 +23,10 @@ export default class extends Controller {
|
|
|
23
23
|
disabled: { type: Boolean, default: false },
|
|
24
24
|
nextPage: { type: Number, default: undefined },
|
|
25
25
|
fullScreenThreshold: { type: Number, default: 1024 },
|
|
26
|
+
typingIndicatorKeepAlive: { type: Number, default: 30000 }, // 30 seconds
|
|
27
|
+
offset: { type: Number, default: 24 },
|
|
28
|
+
padding: { type: Number, default: 24 },
|
|
29
|
+
optimisticTypingIndicatorWait: { type: Number, default: 1000 }, // 1 second
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
static classes = ['fadeOut']
|
|
@@ -40,12 +44,13 @@ export default class extends Controller {
|
|
|
40
44
|
'messageTemplate',
|
|
41
45
|
'messagesContainer',
|
|
42
46
|
'title',
|
|
43
|
-
'onlineStatus',
|
|
44
47
|
'attachmentImage',
|
|
45
48
|
'footer',
|
|
46
49
|
'toolbar',
|
|
47
50
|
'message',
|
|
48
51
|
'unreadCounter',
|
|
52
|
+
'typingIndicator',
|
|
53
|
+
'typingIndicatorTemplate',
|
|
49
54
|
]
|
|
50
55
|
|
|
51
56
|
initialize() {
|
|
@@ -60,9 +65,8 @@ export default class extends Controller {
|
|
|
60
65
|
this.files = []
|
|
61
66
|
|
|
62
67
|
this.onMessageReceived = this.onMessageReceived.bind(this)
|
|
63
|
-
this.onConversationAssignment = this.onConversationAssignment.bind(this)
|
|
64
|
-
this.onAgentOnline = this.onAgentOnline.bind(this)
|
|
65
68
|
this.onMessageReaction = this.onMessageReaction.bind(this)
|
|
69
|
+
this.onTypingStart = this.onTypingStart.bind(this)
|
|
66
70
|
|
|
67
71
|
this.onScroll = this.onScroll.bind(this)
|
|
68
72
|
|
|
@@ -81,9 +85,8 @@ export default class extends Controller {
|
|
|
81
85
|
this.setupFloatingUI({ trigger: this.triggerTarget, popover: this.popoverTarget })
|
|
82
86
|
|
|
83
87
|
this.webChatChannel.onMessage(this.onMessageReceived)
|
|
84
|
-
this.webChatChannel.
|
|
88
|
+
this.webChatChannel.onTypingStart(this.onTypingStart)
|
|
85
89
|
|
|
86
|
-
this.webChatChannel.onAgentOnline(this.onAgentOnline)
|
|
87
90
|
this.webChatChannel.onReaction(this.onMessageReaction)
|
|
88
91
|
|
|
89
92
|
this.messagesContainerTarget.addEventListener('scroll', this.onScroll)
|
|
@@ -106,12 +109,94 @@ export default class extends Controller {
|
|
|
106
109
|
this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent)
|
|
107
110
|
this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
|
|
108
111
|
|
|
112
|
+
// Clean up typing indicator timeouts
|
|
113
|
+
this.clearTypingIndicator()
|
|
114
|
+
|
|
109
115
|
this.broadcastChannel.close()
|
|
110
116
|
this.floatingUICleanup()
|
|
111
117
|
|
|
112
118
|
super.disconnect()
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
onTypingStart() {
|
|
122
|
+
if (this.typingIndicatorVisible) {
|
|
123
|
+
return this.resetTypingIndicatorTimer()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.showTypingIndicator()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
showOptimisticTypingIndicator() {
|
|
130
|
+
if (this.typingIndicatorVisible) return
|
|
131
|
+
|
|
132
|
+
this.showTypingIndicator()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
showTypingIndicator() {
|
|
136
|
+
this.clearTypingIndicator()
|
|
137
|
+
|
|
138
|
+
this.typingIndicatorVisible = true
|
|
139
|
+
const indicator = this.typingIndicatorTemplateTarget.cloneNode(true)
|
|
140
|
+
|
|
141
|
+
indicator.setAttribute('data-hellotext--webchat-target', 'typingIndicator')
|
|
142
|
+
indicator.style.display = 'flex'
|
|
143
|
+
|
|
144
|
+
this.messagesContainerTarget.appendChild(indicator)
|
|
145
|
+
|
|
146
|
+
requestAnimationFrame(() => {
|
|
147
|
+
this.messagesContainerTarget.scroll({
|
|
148
|
+
top: this.messagesContainerTarget.scrollHeight,
|
|
149
|
+
behavior: 'instant',
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const timeout = this.typingIndicatorKeepAliveValue
|
|
154
|
+
|
|
155
|
+
this.incomingTypingIndicatorTimeout = setTimeout(() => {
|
|
156
|
+
this.clearTypingIndicator()
|
|
157
|
+
}, timeout)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resetTypingIndicatorTimer() {
|
|
161
|
+
if (!this.typingIndicatorVisible) return
|
|
162
|
+
|
|
163
|
+
// Clear existing timeout
|
|
164
|
+
clearTimeout(this.incomingTypingIndicatorTimeout)
|
|
165
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
166
|
+
|
|
167
|
+
// Use unified timeout for all typing indicators
|
|
168
|
+
const timeout = this.typingIndicatorKeepAliveValue
|
|
169
|
+
|
|
170
|
+
this.incomingTypingIndicatorTimeout = setTimeout(() => {
|
|
171
|
+
this.clearTypingIndicator()
|
|
172
|
+
}, timeout)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
clearTypingIndicator() {
|
|
176
|
+
if (this.hasTypingIndicatorTarget) {
|
|
177
|
+
this.typingIndicatorTarget.remove()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.typingIndicatorVisible = false
|
|
181
|
+
|
|
182
|
+
clearTimeout(this.incomingTypingIndicatorTimeout)
|
|
183
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onMessageInputChange() {
|
|
187
|
+
this.resizeInput()
|
|
188
|
+
clearTimeout(this.typingIndicatorTimeout)
|
|
189
|
+
|
|
190
|
+
if (!this.hasSentTypingIndicator) {
|
|
191
|
+
this.webChatChannel.startTypingIndicator()
|
|
192
|
+
this.hasSentTypingIndicator = true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.typingIndicatorTimeout = setTimeout(() => {
|
|
196
|
+
this.hasSentTypingIndicator = false
|
|
197
|
+
}, 3000)
|
|
198
|
+
}
|
|
199
|
+
|
|
115
200
|
onOutboundMessageSent(event) {
|
|
116
201
|
const { data } = event
|
|
117
202
|
|
|
@@ -120,7 +205,13 @@ export default class extends Controller {
|
|
|
120
205
|
const element = new DOMParser().parseFromString(data.element, 'text/html').body
|
|
121
206
|
.firstElementChild
|
|
122
207
|
|
|
123
|
-
|
|
208
|
+
// Insert message before typing indicator if one exists
|
|
209
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
210
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
211
|
+
} else {
|
|
212
|
+
this.messagesContainerTarget.appendChild(element)
|
|
213
|
+
}
|
|
214
|
+
|
|
124
215
|
element.scrollIntoView({ behavior: 'instant' })
|
|
125
216
|
},
|
|
126
217
|
'message:failed': data => {
|
|
@@ -295,6 +386,7 @@ export default class extends Controller {
|
|
|
295
386
|
})
|
|
296
387
|
}
|
|
297
388
|
|
|
389
|
+
this.clearTypingIndicator()
|
|
298
390
|
this.messagesContainerTarget.appendChild(element)
|
|
299
391
|
|
|
300
392
|
Hellotext.eventEmitter.dispatch('webchat:message:received', {
|
|
@@ -303,7 +395,6 @@ export default class extends Controller {
|
|
|
303
395
|
})
|
|
304
396
|
|
|
305
397
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
306
|
-
this.setOfflineTimeout()
|
|
307
398
|
|
|
308
399
|
if (this.openValue) {
|
|
309
400
|
this.messagesAPI.markAsSeen(id)
|
|
@@ -316,24 +407,20 @@ export default class extends Controller {
|
|
|
316
407
|
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
317
408
|
}
|
|
318
409
|
|
|
319
|
-
|
|
320
|
-
const
|
|
410
|
+
resizeInput() {
|
|
411
|
+
const maxHeight = 96
|
|
321
412
|
|
|
322
|
-
|
|
413
|
+
// Temporarily reset height to its natural content size
|
|
414
|
+
this.inputTarget.style.height = 'auto'
|
|
323
415
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.onlineStatusTarget.style.display = 'none'
|
|
328
|
-
}
|
|
329
|
-
}
|
|
416
|
+
// Set the height to the scrollHeight, which is the minimum height
|
|
417
|
+
// the element needs to fit its content without a scrollbar.
|
|
418
|
+
const scrollHeight = this.inputTarget.scrollHeight
|
|
330
419
|
|
|
331
|
-
|
|
332
|
-
this.onlineStatusTarget.style.display = 'flex'
|
|
333
|
-
this.setOfflineTimeout()
|
|
420
|
+
this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
|
|
334
421
|
}
|
|
335
422
|
|
|
336
|
-
async sendMessage() {
|
|
423
|
+
async sendMessage(e) {
|
|
337
424
|
const formData = new FormData()
|
|
338
425
|
|
|
339
426
|
const message = {
|
|
@@ -342,6 +429,10 @@ export default class extends Controller {
|
|
|
342
429
|
}
|
|
343
430
|
|
|
344
431
|
if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
|
|
432
|
+
if (e && e.target) {
|
|
433
|
+
e.preventDefault()
|
|
434
|
+
}
|
|
435
|
+
|
|
345
436
|
return
|
|
346
437
|
}
|
|
347
438
|
|
|
@@ -381,7 +472,13 @@ export default class extends Controller {
|
|
|
381
472
|
})
|
|
382
473
|
}
|
|
383
474
|
|
|
384
|
-
|
|
475
|
+
// Insert message before typing indicator if one exists
|
|
476
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
477
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
478
|
+
} else {
|
|
479
|
+
this.messagesContainerTarget.appendChild(element)
|
|
480
|
+
}
|
|
481
|
+
|
|
385
482
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
386
483
|
|
|
387
484
|
this.broadcastChannel.postMessage({
|
|
@@ -390,6 +487,7 @@ export default class extends Controller {
|
|
|
390
487
|
})
|
|
391
488
|
|
|
392
489
|
this.inputTarget.value = ''
|
|
490
|
+
this.resizeInput()
|
|
393
491
|
this.files = []
|
|
394
492
|
|
|
395
493
|
this.attachmentInputTarget.value = ''
|
|
@@ -400,9 +498,21 @@ export default class extends Controller {
|
|
|
400
498
|
|
|
401
499
|
this.inputTarget.focus()
|
|
402
500
|
|
|
501
|
+
// Set up optimistic typing indicator BEFORE making the API call
|
|
502
|
+
// This prevents race conditions with server responses
|
|
503
|
+
if (!this.typingIndicatorVisible) {
|
|
504
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
505
|
+
this.optimisticTypingTimeout = setTimeout(() => {
|
|
506
|
+
this.showOptimisticTypingIndicator()
|
|
507
|
+
}, this.optimisticTypingIndicatorWaitValue)
|
|
508
|
+
}
|
|
509
|
+
|
|
403
510
|
const response = await this.messagesAPI.create(formData)
|
|
404
511
|
|
|
405
512
|
if (response.failed) {
|
|
513
|
+
// Clear the optimistic typing indicator on failure
|
|
514
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
515
|
+
|
|
406
516
|
this.broadcastChannel.postMessage({
|
|
407
517
|
type: 'message:failed',
|
|
408
518
|
id: element.id,
|
|
@@ -422,6 +532,11 @@ export default class extends Controller {
|
|
|
422
532
|
this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)
|
|
423
533
|
}
|
|
424
534
|
|
|
535
|
+
// If there's already a typing indicator visible, reset its timer
|
|
536
|
+
if (this.typingIndicatorVisible) {
|
|
537
|
+
this.resetTypingIndicatorTimer()
|
|
538
|
+
}
|
|
539
|
+
|
|
425
540
|
this.attachmentContainerTarget.style.display = ''
|
|
426
541
|
}
|
|
427
542
|
|
|
@@ -534,24 +649,12 @@ export default class extends Controller {
|
|
|
534
649
|
this.inputTarget.focus()
|
|
535
650
|
}
|
|
536
651
|
|
|
537
|
-
setOfflineTimeout() {
|
|
538
|
-
clearTimeout(this.offlineTimeout)
|
|
539
|
-
|
|
540
|
-
this.offlineTimeout = setTimeout(() => {
|
|
541
|
-
this.onlineStatusTarget.style.display = 'none'
|
|
542
|
-
}, this.fiveMinutes)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
652
|
byteToMegabyte(bytes) {
|
|
546
653
|
return Math.ceil(bytes / 1024 / 1024)
|
|
547
654
|
}
|
|
548
655
|
|
|
549
|
-
get fiveMinutes() {
|
|
550
|
-
return 300000
|
|
551
|
-
}
|
|
552
|
-
|
|
553
656
|
get middlewares() {
|
|
554
|
-
return [offset(
|
|
657
|
+
return [offset(this.offsetValue), shift({ padding: this.paddingValue }), flip()]
|
|
555
658
|
}
|
|
556
659
|
|
|
557
660
|
get shouldOpenOnMount() {
|
package/src/hellotext.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Configuration, Event } from './core'
|
|
2
2
|
|
|
3
3
|
import API, { Response } from './api'
|
|
4
|
-
import { Business, FormCollection, Query, Session, Webchat } from './models'
|
|
4
|
+
import { Business, FormCollection, Page, Query, Session, Webchat } from './models'
|
|
5
5
|
|
|
6
6
|
import { NotInitializedError } from './errors'
|
|
7
7
|
|
|
@@ -23,6 +23,7 @@ class Hellotext {
|
|
|
23
23
|
Configuration.assign(config)
|
|
24
24
|
Session.initialize()
|
|
25
25
|
|
|
26
|
+
this.page = new Page()
|
|
26
27
|
this.business = new Business(business)
|
|
27
28
|
this.forms = new FormCollection()
|
|
28
29
|
|
|
@@ -54,11 +55,13 @@ class Hellotext {
|
|
|
54
55
|
...this.headers,
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const pageInstance = params && params.url ? new Page(params.url) : this.page
|
|
59
|
+
|
|
57
60
|
const body = {
|
|
58
61
|
session: this.session,
|
|
59
62
|
action,
|
|
60
63
|
...params,
|
|
61
|
-
|
|
64
|
+
...pageInstance.trackingData,
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
delete body.headers
|
package/src/models/cookies.js
CHANGED
|
@@ -2,7 +2,7 @@ import Hellotext from '../hellotext'
|
|
|
2
2
|
|
|
3
3
|
class Cookies {
|
|
4
4
|
static set(name, value) {
|
|
5
|
-
if(typeof document !== 'undefined') {
|
|
5
|
+
if (typeof document !== 'undefined') {
|
|
6
6
|
document.cookie = `${name}=${value}; path=/;`
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ class Cookies {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
static get(name) {
|
|
15
|
-
if(typeof document !== 'undefined') {
|
|
15
|
+
if (typeof document !== 'undefined') {
|
|
16
16
|
return document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop()
|
|
17
17
|
} else {
|
|
18
18
|
return undefined
|
package/src/models/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export { Business } from './business'
|
|
|
2
2
|
export { Cookies } from './cookies'
|
|
3
3
|
export { Form } from './form'
|
|
4
4
|
export { FormCollection } from './form_collection'
|
|
5
|
+
export { Page } from './page'
|
|
5
6
|
export { Query } from './query'
|
|
6
7
|
export { Session } from './session'
|
|
8
|
+
export { UTM } from './utm'
|
|
7
9
|
export { Webchat } from './webchat'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { UTM } from './utm'
|
|
2
|
+
|
|
3
|
+
class Page {
|
|
4
|
+
constructor(url = null) {
|
|
5
|
+
this.utm = new UTM()
|
|
6
|
+
this._url = url
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the current page URL
|
|
11
|
+
* @returns {string} The page URL
|
|
12
|
+
*/
|
|
13
|
+
get url() {
|
|
14
|
+
return this._url || window.location.href
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the current page title
|
|
19
|
+
* @returns {string} The page title
|
|
20
|
+
*/
|
|
21
|
+
get title() {
|
|
22
|
+
return document.title
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the current page path
|
|
27
|
+
* @returns {string} The page path
|
|
28
|
+
*/
|
|
29
|
+
get path() {
|
|
30
|
+
if (this._url) {
|
|
31
|
+
try {
|
|
32
|
+
return new URL(this._url).pathname
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return '/'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return window.location.pathname
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get current UTM parameters
|
|
43
|
+
* @returns {Object} The UTM parameters
|
|
44
|
+
*/
|
|
45
|
+
get utmParams() {
|
|
46
|
+
return this.utm.current
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get page data for tracking
|
|
51
|
+
* @returns {Object} Object containing page object and utm_params
|
|
52
|
+
*/
|
|
53
|
+
get trackingData() {
|
|
54
|
+
return {
|
|
55
|
+
page: {
|
|
56
|
+
url: this.url,
|
|
57
|
+
title: this.title,
|
|
58
|
+
path: this.path,
|
|
59
|
+
},
|
|
60
|
+
utm_params: this.utmParams,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Page }
|
package/src/models/query.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Cookies } from './cookies'
|
|
2
|
-
|
|
3
1
|
class Query {
|
|
4
2
|
static get inPreviewMode() {
|
|
5
3
|
return new this().inPreviewMode
|
|
@@ -22,7 +20,7 @@ class Query {
|
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
get session() {
|
|
25
|
-
return this.get('session')
|
|
23
|
+
return this.get('session')
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
toHellotextParam(param) {
|
package/src/models/session.js
CHANGED
|
@@ -18,7 +18,7 @@ class Session {
|
|
|
18
18
|
static initialize() {
|
|
19
19
|
this.#query = new Query()
|
|
20
20
|
|
|
21
|
-
this.session =
|
|
21
|
+
this.session = this.#query.session || Configuration.session || Cookies.get('hello_session')
|
|
22
22
|
|
|
23
23
|
if (!this.session && Configuration.autoGenerateSession) {
|
|
24
24
|
this.session = crypto.randomUUID()
|