@subiz/widget-v4 4.1.210 → 4.1.212

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 (130) hide show
  1. package/README.md +21 -2
  2. package/assets/img/dot_chat.svg +3 -0
  3. package/assets/less/chatbox.less +9 -0
  4. package/assets/less/form.less +10 -0
  5. package/assets/less/message.less +114 -13
  6. package/assets/less/widget.less +15 -18
  7. package/compiled_src/call/css.js +1 -1
  8. package/compiled_src/chatbox/Buttons.js +7 -4
  9. package/compiled_src/chatbox/ChatButton.js +127 -29
  10. package/compiled_src/chatbox/Event.js +37 -5
  11. package/compiled_src/chatbox/Events.js +348 -129
  12. package/compiled_src/chatbox/FormAttachment.js +1 -1
  13. package/compiled_src/chatbox/Header.js +7 -2
  14. package/compiled_src/chatbox/InfoMessage.js +1 -1
  15. package/compiled_src/chatbox/MessageInput.js +24 -9
  16. package/compiled_src/chatbox/OrderAttachments.js +13 -13
  17. package/compiled_src/chatbox/QuoteMessage.js +3 -2
  18. package/compiled_src/chatbox/TextMessage.js +22 -18
  19. package/compiled_src/chatbox/Widget.js +200 -129
  20. package/compiled_src/chatbox/block.js +39 -29
  21. package/compiled_src/chatbox/conversation-history.js +44 -0
  22. package/compiled_src/chatbox/event-layout.js +59 -0
  23. package/compiled_src/chatbox/greeting.js +32 -0
  24. package/compiled_src/chatbox/main.js +3 -0
  25. package/compiled_src/common.js +13 -9
  26. package/compiled_src/form/css.js +1 -1
  27. package/compiled_src/lib.js +22 -32
  28. package/compiled_src/notif/css.js +1 -1
  29. package/compiled_src/plugin.js +11 -18
  30. package/compiled_src/popup/css.js +1 -1
  31. package/compiled_src/popup/popup.js +1 -1
  32. package/compiled_src/store/api.js +0 -2
  33. package/compiled_src/store/api_fake.js +1 -1
  34. package/compiled_src/store/attachments.js +28 -0
  35. package/compiled_src/store/chain.js +24 -10
  36. package/compiled_src/store/kv.js +9 -13
  37. package/compiled_src/store/lsdb.js +1 -1
  38. package/compiled_src/store/store.js +356 -117
  39. package/compiled_src/util/common.js +48 -41
  40. package/compiled_src/util/event.js +15 -0
  41. package/compiled_src/util/pubsub.js +28 -13
  42. package/compiled_src/util/url.js +18 -0
  43. package/dist/127.js +13 -13
  44. package/dist/132.js +1 -1
  45. package/dist/1a0f6f3ff1a.127.js +1 -0
  46. package/dist/{1a0d80f6e72.132.js → 1a0f6f3ff1a.132.js} +1 -1
  47. package/dist/{1a0d80f6e72.27.js → 1a0f6f3ff1a.27.js} +1 -1
  48. package/dist/1a0f6f3ff1a.278.js +1 -0
  49. package/dist/{1a0d80f6e72.339.js → 1a0f6f3ff1a.339.js} +1 -1
  50. package/dist/1a0f6f3ff1a.459.js +1 -0
  51. package/dist/1a0f6f3ff1a.app.js +1 -0
  52. package/dist/{1a0d80f6e72.chat.html → 1a0f6f3ff1a.chat.html} +1 -1
  53. package/dist/1a0f6f3ff1a.chat.js +1 -0
  54. package/dist/{1a0d80f6e72.form.html → 1a0f6f3ff1a.form.html} +1 -1
  55. package/dist/1a0f6f3ff1a.form.js +1 -0
  56. package/dist/{1a0d80f6e72.index.html → 1a0f6f3ff1a.index.html} +1 -1
  57. package/dist/{1a0d80f6e72.library.html → 1a0f6f3ff1a.library.html} +1 -1
  58. package/dist/1a0f6f3ff1a.library.js +1 -0
  59. package/dist/{1a0d80f6e72.preview.html → 1a0f6f3ff1a.preview.html} +1 -1
  60. package/dist/1a0f6f3ff1a.preview.js +1 -0
  61. package/dist/1a0f6f3ff1a.webview.js +1 -0
  62. package/dist/27.js +2 -2
  63. package/dist/459.js +2 -2
  64. package/dist/HASH +1 -1
  65. package/dist/assets/img/dot_chat.335fd8a.svg +3 -0
  66. package/dist/screenshoot.js +1523 -619
  67. package/package.json +12 -5
  68. package/src/chatbox/Buttons.js +5 -4
  69. package/src/chatbox/ChatButton.js +133 -39
  70. package/src/chatbox/Event.js +38 -5
  71. package/src/chatbox/Events.js +306 -134
  72. package/src/chatbox/FormAttachment.js +1 -1
  73. package/src/chatbox/Header.js +5 -4
  74. package/src/chatbox/InfoMessage.js +1 -1
  75. package/src/chatbox/MessageInput.js +15 -10
  76. package/src/chatbox/OrderAttachments.js +13 -13
  77. package/src/chatbox/QuoteMessage.js +3 -2
  78. package/src/chatbox/TextMessage.js +21 -20
  79. package/src/chatbox/Widget.js +150 -150
  80. package/src/chatbox/block.js +29 -27
  81. package/src/chatbox/conversation-history.js +37 -0
  82. package/src/chatbox/event-layout.js +72 -0
  83. package/src/chatbox/greeting.js +13 -0
  84. package/src/chatbox/main.js +3 -0
  85. package/src/common.js +15 -9
  86. package/src/lib.js +22 -35
  87. package/src/plugin.js +11 -13
  88. package/src/popup/popup.js +1 -1
  89. package/src/store/api.js +0 -2
  90. package/src/store/api_fake.js +1 -1
  91. package/src/store/attachments.js +21 -0
  92. package/src/store/chain.js +25 -12
  93. package/src/store/kv.js +10 -19
  94. package/src/store/lsdb.js +1 -1
  95. package/src/store/store.js +313 -130
  96. package/src/util/common.js +48 -43
  97. package/src/util/event.js +7 -0
  98. package/src/util/pubsub.js +27 -8
  99. package/src/util/url.js +14 -0
  100. package/test.cjs +59 -0
  101. package/assets/less/contact.less +0 -126
  102. package/compiled_src/util/common.test.js +0 -414
  103. package/compiled_src/util/event.test.js +0 -38
  104. package/compiled_src/util/extractor.test.js +0 -299
  105. package/compiled_src/util/extractor.test2.js +0 -61
  106. package/compiled_src/util/gate.test.js +0 -18
  107. package/compiled_src/util/pubsub.test.js +0 -35
  108. package/dist/1a0d80f6e72.127.js +0 -1
  109. package/dist/1a0d80f6e72.278.js +0 -1
  110. package/dist/1a0d80f6e72.459.js +0 -1
  111. package/dist/1a0d80f6e72.74.js +0 -1
  112. package/dist/1a0d80f6e72.997.js +0 -1
  113. package/dist/1a0d80f6e72.app.js +0 -1
  114. package/dist/1a0d80f6e72.chat.js +0 -1
  115. package/dist/1a0d80f6e72.form.js +0 -1
  116. package/dist/1a0d80f6e72.library.js +0 -1
  117. package/dist/1a0d80f6e72.preview.js +0 -1
  118. package/dist/1a0d80f6e72.webview.js +0 -1
  119. package/dist/74.js +0 -376
  120. package/dist/997.js +0 -37
  121. package/src/contact/contact.js +0 -400
  122. package/src/contact/css.js +0 -1
  123. package/src/util/common.test.js +0 -301
  124. package/src/util/event.test.js +0 -31
  125. package/src/util/extractor.test.js +0 -240
  126. package/src/util/extractor.test2.js +0 -56
  127. package/src/util/gate.test.js +0 -20
  128. package/src/util/pubsub.test.js +0 -37
  129. /package/dist/{1a0d80f6e72.185.js → 1a0f6f3ff1a.185.js} +0 -0
  130. /package/dist/{1a0d80f6e72.490.js → 1a0f6f3ff1a.490.js} +0 -0
@@ -8,6 +8,7 @@ var checkedimg = require('../../assets/img/done_gray.svg')
8
8
  var empty = require('../../assets/img/empty.svg')
9
9
  import MessageInput from './MessageInput.js'
10
10
  import Events from './Events.js'
11
+ import {widgetGreeting} from './greeting.js'
11
12
  // import Typing from './Typing.js'
12
13
  import Rating from './Rating.js'
13
14
  import ConnectionStatus from './ConnectionStatus.js'
@@ -28,7 +29,6 @@ export default class Widget extends Com {
28
29
  super(props)
29
30
  this.userSent = {}
30
31
 
31
- this.debugAgentSentEvIds = {}
32
32
  // check user had input form
33
33
  this.wasFormInputed = false
34
34
  this.setState({
@@ -50,56 +50,6 @@ export default class Widget extends Com {
50
50
  if (!this.props.store.isMobile()) this.focusInput()
51
51
 
52
52
  this.updateFormDataFields()
53
- this.debugRTAgentMessageSent()
54
- }
55
-
56
- findMissingRTMessage = async (started) => {
57
- let messageSentDict = this.props.store.listAgentRealtimeMessageSent()
58
-
59
- let res1 = await this.props.store.sbzListConversations()
60
- let conversions = sb.get(res1, 'body.conversations') || []
61
- let cids = sb.map(conversions, (convo) => convo.id)
62
-
63
- let promises = sb.map(cids, (cid) => this.props.store.sbzListMessages(cid))
64
- let msgReses = await Promise.all(promises)
65
- sb.map(msgReses, (msgRes) => {
66
- let messages = sb.get(msgRes, 'body') || []
67
- sb.map(messages, (ev) => {
68
- if (ev.created < started) return
69
- //if (ev.created > Date.now() - 30_000) return
70
- if (ev.type !== 'message_sent') return
71
- if (sb.get(ev, 'by.type') === 'user') return
72
-
73
- console.log('check ev pass condition', ev)
74
- if (!messageSentDict[ev.id] && !this.debugAgentSentEvIds[ev.id]) {
75
- let accid = sb.get(this.props.store.matchAccount(), 'id')
76
- this.props.store.sendBugReport(
77
- `[WIDGET ${accid}] Missing agent message sent evid ${ev.id}, cid ${sb.convoOfEv(ev)}, ev_created ${ev.created}, FE timestamp ${Date.now()}, Init Debug timestamp ${started}`,
78
- )
79
- this.debugAgentSentEvIds[ev.id] = true
80
- }
81
- })
82
- })
83
- }
84
-
85
- debugRTAgentMessageSent = async () => {
86
- try {
87
- let debugAccountIds = this.props.store.matchDebugAccountIds()
88
- let accid = sb.get(this.props.store.matchAccount(), 'id')
89
- if (!debugAccountIds.includes(accid)) return
90
-
91
- console.log('START check RT agent message with BE source')
92
- let started = Date.now()
93
- this.findMissingRTMessage(started)
94
-
95
- for (let i = 0; i < 100_000; i++) {
96
- await sb.sleep(120_000)
97
- console.log('LOOP check RT agent message with BE source', i)
98
- this.findMissingRTMessage(started)
99
- }
100
- } catch (err) {
101
- this.props.store.sendBugReport(`debugRTAgentMessageSent ERROR ${err.stack}`)
102
- }
103
53
  }
104
54
 
105
55
  applyMessage(text) {
@@ -115,7 +65,7 @@ export default class Widget extends Com {
115
65
 
116
66
  if (prevProp.cid !== this.props.cid) {
117
67
  this.closeOrder()
118
- this.setState({clickedJoin: false, quickReplyMessage: {}})
68
+ this.setState({clickedJoin: false, restartForm: false, quickReplyMessage: {}})
119
69
  sb.setTimeout(() => {
120
70
  // focus Events then focus MessageInput
121
71
  // if there is any pending form, MessageInput will be disabled
@@ -128,23 +78,16 @@ export default class Widget extends Com {
128
78
  // this.updateFormDataFields()
129
79
  }
130
80
 
81
+ componentWillUnmount() {
82
+ this.cancelContainerFocus()
83
+ }
84
+
131
85
  ShowTyping() {
132
86
  this.$events && this.$events.ShowTyping()
133
87
  }
134
88
 
135
89
  shouldShowForm() {
136
- // always show form when ask agentgroup
137
- let form
138
- let accountSetting = this.props.plugin.chatbox || {}
139
- let acc = this.props.store.matchAccount() || {}
140
- if (sb.get(accountSetting, 'prechat_form.enabled')) form = accountSetting.prechat_form
141
-
142
- let offlineEnabled = sb.get(accountSetting, 'offline_form.enabled')
143
- if (this.isOffline(acc, accountSetting) && offlineEnabled) form = accountSetting.offline_form
144
-
145
- if (!this.hasSentMessage(this.props.cid) && form && (sb.size(form.groups) || form.group_required)) return true
146
- let formRequired = this.isFormRequired() // nhap thieu truong require
147
- return (formRequired && !this.props.cid) || (formRequired && this.props.cid && this.state.clickedJoin)
90
+ return !!(this.state.restartForm || this.state.clickedJoin)
148
91
  }
149
92
 
150
93
  render() {
@@ -226,38 +169,18 @@ export default class Widget extends Com {
226
169
 
227
170
  if (
228
171
  field.is_required &&
229
- (field.value === undefined || field.value === '' || (sb.size(field.value) === 0 && Array.isArray(field.value)))
172
+ (field.value == null || (typeof field.value === 'string' && !field.value.trim()) || (sb.size(field.value) === 0 && Array.isArray(field.value)))
230
173
  ) {
231
174
  field.is_empty_error = true
232
175
  field.error = this.props.t('error_empty')
233
176
  } else field.error = ''
234
177
 
235
- if (key.substr(5) === 'emails') {
236
- if (field.is_required) {
237
- if (!sb.isEmail(field.value)) {
238
- field.error = this.props.t('error_email_invalid') // 'The email is not valid'
239
- }
240
- } else {
241
- let value = field.value || ''
242
- if (typeof value !== 'string') value = value + '' // try to makesure use ''.trim doesnt errror
243
- if (value.trim() && !sb.isEmail(field.value)) {
244
- field.error = this.props.t('error_email_invalid') // 'The email is not valid'
245
- }
246
- }
178
+ if (key === 'attr:emails' && !field.error && String(field.value ?? '').trim() && !sb.isEmail(field.value)) {
179
+ field.error = this.props.t('error_email_invalid')
247
180
  }
248
181
 
249
- if (key.substr(5) === 'phones') {
250
- if (field.is_required) {
251
- if (!sb.isPhone(field.value)) {
252
- field.error = this.props.t('error_phone_invalid') // 'The email is not valid'
253
- }
254
- } else {
255
- let value = field.value || ''
256
- if (typeof value !== 'string') value = value + '' // try to makesure use ''.trim doesnt errror
257
- if (value.trim() && !sb.isPhone(field.value)) {
258
- field.error = this.props.t('error_phone_invalid') // 'The email is not valid'
259
- }
260
- }
182
+ if (key === 'attr:phones' && !field.error && String(field.value ?? '').trim() && !sb.isPhone(field.value)) {
183
+ field.error = this.props.t('error_phone_invalid')
261
184
  }
262
185
  newformdata[key] = field
263
186
  })
@@ -265,7 +188,7 @@ export default class Widget extends Com {
265
188
  }
266
189
 
267
190
  async submitPrechatForm(form, formName) {
268
- if (this.state.submiting) return
191
+ if (this.state.submiting || this.state.creating) return
269
192
  // check error first
270
193
  let newData = this.validateForm(this.state.formData)
271
194
  let hasError = sb.find(newData, (field) => !!field.error)
@@ -282,6 +205,8 @@ export default class Widget extends Com {
282
205
  let is_lead = false
283
206
  sb.map(data, (field, key) => {
284
207
  if (!key.startsWith('attr:')) return
208
+ if (key === 'attr:phones') field.value = String(field.value ?? '').trim().replace(/[(). \-]/g, '')
209
+ if (key === 'attr:emails') field.value = String(field.value ?? '').trim()
285
210
  var attr = {key: key.substr(5)}
286
211
  is_lead = is_lead || key == 'emails' || key == 'phones'
287
212
  if (field.type === 'number') attr.number = parseInt(field.value)
@@ -293,7 +218,8 @@ export default class Widget extends Com {
293
218
 
294
219
  if (is_lead) sb.setUserAttr(user, {key: 'lead_source', text: 'widget'})
295
220
 
296
- let firstmsg = sb.get(data, 'question.value', this.props.t('get_started'))
221
+ const question = String(sb.get(data, 'question.value', '') ?? '').trim()
222
+ const firstmsg = question || (/^vi(?:[-_]|$)/i.test(navigator.language || '') ? 'Xin chào' : 'Hello')
297
223
  let groupid = sb.get(data, 'group.value', '')
298
224
 
299
225
  // creating convo
@@ -309,15 +235,15 @@ export default class Widget extends Com {
309
235
 
310
236
  if (_creating != this._creating) return // outdated
311
237
  if (this.props.cid && this.props.cid.startsWith('cslight')) {
312
- this.sendMessage({
238
+ await this.sendMessage({
313
239
  text: firstmsg,
314
240
  format: 'plaintext',
315
241
  fields: [{key: 'to_agent_group', value: sb.stringify(groupid)}],
316
242
  attachments: this.state.quickReplyMessage.attachments,
317
- })
243
+ }, {formSubmitted: true, groupId: groupid})
318
244
  return
319
245
  }
320
- if (this.props.cid) {
246
+ if (this.props.cid && sb.get(this.props.store.matchConvo(this.props.cid), 'state') !== 'ended') {
321
247
  // this.props.store.setUser(user)
322
248
  this.setState({clickedJoin: false})
323
249
  // form will auto disappear after rerender
@@ -339,7 +265,7 @@ export default class Widget extends Com {
339
265
  return
340
266
  }
341
267
 
342
- let message = {text: firstmsg, format: 'plaintext'}
268
+ let message = {text: firstmsg, format: 'plaintext', attachments: this.state.quickReplyMessage.attachments}
343
269
  let req = {group_id: groupid, message: firstmsg, locale: this.props.t.locale(), initial_by: user.id}
344
270
 
345
271
  this.setState({creatingError: null})
@@ -355,9 +281,12 @@ export default class Widget extends Com {
355
281
  this.userSent[newconvoid] = true
356
282
  await this.props.store.sendConvoEvent(newconvoid, 'message_sent', {message})
357
283
  if (_creating != this._creating) return // outdated
284
+ if (form.end_conversation_after_submit) await this.props.store.endConvo(newconvoid)
285
+ if (_creating != this._creating) return
358
286
  this.setState({
359
287
  creating: false,
360
288
  clickedJoin: false,
289
+ restartForm: false,
361
290
  quickReplyMessage: {},
362
291
  })
363
292
  this.props.onGo({view: 'chat', cid: newconvoid})
@@ -370,7 +299,7 @@ export default class Widget extends Com {
370
299
  if (form.question_required || this.state.quickReplyMessage.text) {
371
300
  formData['question'] = {
372
301
  key: 'question',
373
- is_required: true,
302
+ is_required: false,
374
303
  label: this.props.t.i18n(sb.get(form, `i18n_question_label`, {})) || form.question_label,
375
304
  type: 'text',
376
305
  value: this.state.quickReplyMessage.text,
@@ -404,6 +333,7 @@ export default class Widget extends Com {
404
333
  if (!field) return
405
334
  field.value = val
406
335
  field.error = undefined
336
+ field.is_empty_error = false
407
337
  this.setState({formData})
408
338
  }
409
339
 
@@ -546,8 +476,8 @@ export default class Widget extends Com {
546
476
  renderFields(form, formName) {
547
477
  if (sb.size(this.state.formData) === 0) return null
548
478
  let $fields = sb.map(this.state.formData, (field, key) => {
549
- // not display question when click bot quick reply
550
- if (this.state.quickReplyMessage.text && key === 'question') return null
479
+ // Bot quick replies already display their selected answer.
480
+ if (key === 'question' && sb.find(this.state.quickReplyMessage.attachments, att => att.type === 'postback')) return null
551
481
 
552
482
  let $field = null
553
483
  var errclass = field.error ? 'input--error' : ''
@@ -584,7 +514,11 @@ export default class Widget extends Com {
584
514
  this['form_' + encode(key)] = ref
585
515
  }}
586
516
  class={`form-control ${errclass}`}
587
- type='text'
517
+ type={key === 'attr:phones' ? 'tel' : key === 'attr:emails' ? 'email' : 'text'}
518
+ inputMode={key === 'attr:phones' ? 'tel' : key === 'attr:emails' ? 'email' : undefined}
519
+ autoComplete={key === 'attr:phones' ? 'tel' : key === 'attr:emails' ? 'email' : undefined}
520
+ aria-invalid={field.error ? 'true' : undefined}
521
+ aria-describedby={field.error ? 'form-error_' + encode(key) : undefined}
588
522
  />
589
523
  )
590
524
  }
@@ -680,7 +614,7 @@ export default class Widget extends Com {
680
614
  {field.is_required ? <span class='red'>*</span> : ''}
681
615
  </label>
682
616
  {$field}
683
- {field.error ? <div class='form-control--valid-input--error'>{field.error}</div> : ''}
617
+ {field.error ? <div id={'form-error_' + encode(key)} role='alert' class='form-control--valid-input--error'>{field.error}</div> : ''}
684
618
  </div>
685
619
  )
686
620
  })
@@ -693,9 +627,10 @@ export default class Widget extends Com {
693
627
  // replace ↵ character
694
628
  title = title.replace('↵', '')
695
629
  let btncls = 'btn--start-convo '
696
- let hasError = sb.find(this.state.formData, (field) => !!field.error)
697
630
  let newData = this.validateForm(this.state.formData)
698
631
  let hasEmptyError = sb.find(newData, (field) => !!field.is_empty_error)
632
+ const phone = String(sb.get(this.state.formData, 'attr:phones.value', '') ?? '').trim()
633
+ const phoneTooShort = phone.length > 0 && phone.length < 5
699
634
  let bodycls = 'widget-body'
700
635
  if (this.props.store.isEmbededChatMode()) bodycls += ' embed-chat'
701
636
  return (
@@ -714,7 +649,7 @@ export default class Widget extends Com {
714
649
  <button
715
650
  class={btncls}
716
651
  onClick={(_) => this.submitPrechatForm(form, formName)}
717
- disabled={!!hasEmptyError || !!hasError || this.state.creating}
652
+ disabled={!!hasEmptyError || phoneTooShort || this.state.creating}
718
653
  >
719
654
  {submitLabel}
720
655
  </button>
@@ -724,7 +659,19 @@ export default class Widget extends Com {
724
659
  }
725
660
 
726
661
  goToConvo(cid) {
727
- this.props.onGo({view: 'chat', cid})
662
+ let view = 'chat'
663
+ if (!cid) {
664
+ const greeting = widgetGreeting(this.props.plugin.chatbox, this.props.t)
665
+ cid = this.props.store.startWidgetConvo(greeting, this.props.t.locale()).convo.id
666
+ const form = this.startingConversationForm()
667
+ const setting = this.props.plugin.chatbox || {}
668
+ const offline = sb.get(setting, 'offline_form.enabled') && this.isOffline(this.props.store.matchAccount() || {}, setting)
669
+ if (form && (offline || this.isFormRequired(cid) || sb.size(form.groups) || form.group_required)) {
670
+ view = offline ? 'offline_form' : 'prechat_form'
671
+ this.setState({clickedJoin: false, restartForm: false, quickReplyMessage: {}})
672
+ }
673
+ }
674
+ this.props.onGo({view, cid})
728
675
  }
729
676
 
730
677
  renderConvoList() {
@@ -794,7 +741,7 @@ export default class Widget extends Com {
794
741
  sb.setTimeout(() => $input && $input.blur(), 300) // force lose focus
795
742
  }
796
743
 
797
- focusInput(text) {
744
+ focusInput(text, repeat = true) {
798
745
  if (this.props.store.isPreview) return
799
746
  if (this.shouldShowForm()) {
800
747
  // focus on the element
@@ -821,6 +768,7 @@ export default class Widget extends Com {
821
768
  if (text != undefined && typeof text === 'string') {
822
769
  $input.applyMessage(text)
823
770
  }
771
+ if (!repeat) return $input.focus()
824
772
  sb.setTimeout(() => $input.focus())
825
773
  sb.setTimeout(() => $input.focus(), 300) // force focus
826
774
  }
@@ -837,16 +785,72 @@ export default class Widget extends Com {
837
785
  this.setState({formData, quickReplyMessage: message})
838
786
  }
839
787
 
840
- sendMessage = async (message) => {
841
- // hot fix click quick reply with prechat with display prechat form
842
- let hasPostback = sb.find(message.attachments, (att) => att.type === 'postback')
843
- if (this.isFormRequired() && hasPostback) {
844
- this.onStartConvoClick()
845
- this.fillQuickReplyToQuestion(message)
788
+ sendMessage = async (message, {formSubmitted = false, groupId} = {}) => {
789
+ if (this._sendingNewConversation) return false
790
+ if (sb.get(this.props.store.matchConvo(this.props.cid), 'state') === 'ended') {
791
+ const form = this.startingConversationForm()
792
+ if (form && (sb.size(form.groups) || form.group_required || this.isFormRequired(''))) {
793
+ this.setState({restartForm: true, quickReplyMessage: message})
794
+ return
795
+ }
796
+ this._sendingNewConversation = true
797
+ this.setState({creating: true, creatingError: null})
798
+ try {
799
+ const {convo, error} = await this.props.store.startConvo({
800
+ locale: this.props.t.locale(),
801
+ message: message.text,
802
+ initial_by: this.props.store.matchUser().id,
803
+ })
804
+ if (error) {
805
+ this.setState({creatingError: error})
806
+ return false
807
+ }
808
+ await this.props.store.sendConvoEvent(convo.id, 'message_sent', {message})
809
+ this.props.onGo({view: 'chat', cid: convo.id})
810
+ } finally {
811
+ this._sendingNewConversation = false
812
+ this.setState({creating: false})
813
+ }
814
+ return
815
+ }
816
+ const form = this.startingConversationForm()
817
+ const needsGroup = form && !this.hasSentMessage(this.props.cid) && (sb.size(form.groups) || form.group_required)
818
+ if (!formSubmitted && (this.isFormRequired() || needsGroup)) {
819
+ this.setState({clickedJoin: true, quickReplyMessage: message})
846
820
  return
847
821
  }
848
822
 
849
823
  let text = message.text
824
+ const localConvo = this.props.store.matchConvo(this.props.cid)
825
+ if (localConvo && localConvo.light_type === 'widget') {
826
+ this._sendingNewConversation = true
827
+ this.setState({creating: true, creatingError: null})
828
+ try {
829
+ const {convo, error} = await this.props.store.startConvo({
830
+ locale: localConvo.locale, message: text, group_id: groupId,
831
+ initial_by: this.props.store.matchUser().id,
832
+ greeting_message: localConvo.events[0],
833
+ })
834
+ if (error || !convo) {
835
+ this.setState({creatingError: error || 'start_failed'})
836
+ return false
837
+ }
838
+ this.props.store.promoteWidgetConvo(localConvo.id, convo.id)
839
+ const sending = this.props.store.sendConvoEvent(convo.id, 'message_sent', {message})
840
+ this.props.onGo({view: 'chat', cid: convo.id})
841
+ await sending
842
+ this.userSent[convo.id] = true
843
+ if (formSubmitted && form && form.end_conversation_after_submit) await this.props.store.endConvo(convo.id)
844
+ this.setState({clickedJoin: false, restartForm: false, quickReplyMessage: {}})
845
+ } catch (error) {
846
+ this.setState({creatingError: error})
847
+ return false
848
+ } finally {
849
+ this._sendingNewConversation = false
850
+ this.setState({creating: false})
851
+ }
852
+ return
853
+ }
850
854
 
851
855
  if (this.props.cid && this.props.cid.startsWith('cslight')) {
852
856
  let cid = this.props.cid
@@ -896,9 +900,10 @@ export default class Widget extends Com {
896
900
  return
897
901
  // show error and try again
898
902
  }
899
- await this.props.store.sendConvoEvent(convo.id, 'message_sent', {message})
900
- if (_creating != this._creating) return // outdated
903
+ const sending = this.props.store.sendConvoEvent(convo.id, 'message_sent', {message})
901
904
  this.props.onGo({view: 'chat', cid: convo.id})
905
+ await sending
906
+ if (_creating != this._creating) return // outdated
902
907
  this.setState({creating: false})
903
908
  return
904
909
  }
@@ -929,21 +934,11 @@ export default class Widget extends Com {
929
934
  t={this.props.t}
930
935
  />
931
936
  )
932
- let convo = this.props.store.matchConvo(this.props.cid) || {}
933
- if (this.isFormRequired() || convo.state === 'ended') {
934
- $input = (
935
- <div class='message-input message-input--preform'>
936
- <button class='btn--start-convo' onClick={(e) => this.onStartConvoClick(e)}>
937
- {this.props.t('offline_preform_btn')}
938
- </button>
939
- </div>
940
- )
941
- }
942
937
  let bodycls = 'widget-body'
943
938
  if (this.props.store.isEmbededChatMode()) bodycls += ' embed-chat'
944
939
  return (
945
940
  <div class={bodycls}>
946
- <div class='widget-body--content' onClick={(e) => this.onContainerClick(e)}>
941
+ <div class='widget-body--content' onPointerDown={() => this.cancelContainerFocus()} onClick={(e) => this.onContainerClick(e)}>
947
942
  <Events
948
943
  isShow={this.props.isShow}
949
944
  onLink={this.onLink}
@@ -952,7 +947,7 @@ export default class Widget extends Com {
952
947
  onFormSubmit={(e) => this.onEventFormSubmit(e)}
953
948
  ref={(ref) => (this.$events = ref)}
954
949
  cid={this.props.cid}
955
- creating={this.creating}
950
+ creating={this.state.creating}
956
951
  onSend={(e) => this.sendMessage(e)}
957
952
  onPostback={(e) => this.createPostback(e)}
958
953
  t={this.props.t}
@@ -1004,48 +999,53 @@ export default class Widget extends Com {
1004
999
  this.focusInput()
1005
1000
  }
1006
1001
 
1007
- // end or visit unsent convo which require form
1008
- async onStartConvoClick() {
1009
- var convo = this.props.store.matchConvo(this.props.cid) || {}
1010
- if (convo.state === 'ended') return this.props.onGo({view: 'chat', cid: ''})
1011
- this.setState({clickedJoin: true, quickReplyMessage: {}})
1012
- }
1013
-
1014
1002
  onEventFormSubmit() {
1015
1003
  this.focusInput()
1016
1004
  }
1017
1005
 
1018
- onContainerClick() {
1019
- // ignore select behaviour
1020
- var selection = window.getSelection()
1021
- if (selection.toString().length > 0) return
1022
- // focus text input on desktop
1023
- if (!this.props.store.isMobile()) this.focusInput()
1006
+ cancelContainerFocus() {
1007
+ if (this.containerFocusTimer) sb.clearTimeout(this.containerFocusTimer)
1008
+ this.containerFocusTimer = null
1009
+ }
1010
+
1011
+ onContainerClick(event) {
1012
+ this.cancelContainerFocus()
1013
+ const target = event && event.target
1014
+ const element = target && (target.nodeType === 1 ? target : target.parentElement)
1015
+ if (element && element.closest('a, button, input, textarea, select, [contenteditable]')) return
1016
+ if (event && event.detail > 1) return
1017
+ if (this.props.store.isMobile() || window.getSelection()?.toString()) return
1018
+ const cid = this.props.cid
1019
+ // Give a second click or a drag time to select text before moving focus.
1020
+ this.containerFocusTimer = sb.setTimeout(() => {
1021
+ this.containerFocusTimer = null
1022
+ if (!this._isMounted || this.props.cid !== cid || this.props.isShow === false || window.getSelection()?.toString()) return
1023
+ this.focusInput(undefined, false)
1024
+ }, 300)
1025
+ }
1026
+
1027
+ startingConversationForm() {
1028
+ const setting = this.props.plugin.chatbox || {}
1029
+ const account = this.props.store.matchAccount() || {}
1030
+ if (this.isOffline(account, setting) && sb.get(setting, 'offline_form.enabled')) return setting.offline_form
1031
+ if (sb.get(setting, 'prechat_form.enabled')) return setting.prechat_form
1024
1032
  }
1025
1033
 
1026
- isFormRequired() {
1034
+ isFormRequired(cid = this.props.cid) {
1027
1035
  // neu isRequiredForm = true nhung form = null => show button khi click vao thi show form
1028
1036
 
1029
1037
  // isRequiredForm tra ve true neu (nguoi dung chua tung tra loi)
1030
1038
  // + hoi thoai da dc phan phoi nhung chua nhap du cac truong (nho exclude group, bo qua ca truong message)
1031
1039
  // + hoi thoai moi hoac chua duoc phan phoi va chua nhap du cac truong (dac biet la group). Neu k co group, ma cac truong da dc nhap du, ko quan tam toi da nhap msg hay chua, isrequiredform = false
1032
1040
 
1033
- let form
1034
- // choose form to display
1035
- let accountSetting = this.props.plugin.chatbox || {}
1036
- let acc = this.props.store.matchAccount() || {}
1037
- if (sb.get(accountSetting, 'prechat_form.enabled')) form = accountSetting.prechat_form
1038
-
1039
- //auto detect form
1040
- let offlineEnabled = sb.get(accountSetting, 'offline_form.enabled')
1041
- if (this.isOffline(acc, accountSetting) && offlineEnabled) form = accountSetting.offline_form
1041
+ let form = this.startingConversationForm()
1042
1042
 
1043
1043
  if (!form) return false
1044
1044
 
1045
1045
  let formData = this.fillForm(form)
1046
1046
 
1047
1047
  // dont care about group if the conversation is assigned
1048
- let convo = this.props.store.matchConvo(this.props.cid) || {}
1048
+ let convo = this.props.store.matchConvo(cid) || {}
1049
1049
 
1050
1050
  if (convo.state === 'ended') return true
1051
1051
  // allow button "Tiep tuc chat" display when sent quick_replies msg
@@ -1057,7 +1057,7 @@ export default class Widget extends Com {
1057
1057
  sb.each(formData, (field) => {
1058
1058
  if (field.value) isAllFiledEmpty = false
1059
1059
  })
1060
- if (isAllFiledEmpty && !this.userHasSent(this.props.cid)) return true
1060
+ if (isAllFiledEmpty && !this.userHasSent(cid)) return true
1061
1061
 
1062
1062
  return !!sb.find(formData, (field) => field.is_required && (field.value == null || field.value === ''))
1063
1063
  }
@@ -1226,7 +1226,7 @@ export default class Widget extends Com {
1226
1226
  // add question field
1227
1227
  data['question'] = {
1228
1228
  key: 'question',
1229
- is_required: true,
1229
+ is_required: false,
1230
1230
  label: this.props.t.i18n(sb.get(form, `i18n_question_label`, {})) || form.question_label,
1231
1231
  type: 'text',
1232
1232
  }