@subiz/widget-v4 4.1.209 → 4.1.211

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 (137) hide show
  1. package/README.md +21 -2
  2. package/assets/less/chatbox.less +9 -0
  3. package/assets/less/form.less +10 -0
  4. package/assets/less/message.less +114 -13
  5. package/assets/less/widget.less +21 -16
  6. package/compiled_src/call/css.js +1 -1
  7. package/compiled_src/chatbox/Buttons.js +7 -4
  8. package/compiled_src/chatbox/ChatButton.js +115 -29
  9. package/compiled_src/chatbox/Event.js +37 -5
  10. package/compiled_src/chatbox/Events.js +348 -129
  11. package/compiled_src/chatbox/FormAttachment.js +1 -1
  12. package/compiled_src/chatbox/Header.js +6 -2
  13. package/compiled_src/chatbox/InfoMessage.js +1 -1
  14. package/compiled_src/chatbox/MessageInput.js +24 -9
  15. package/compiled_src/chatbox/OrderAttachments.js +13 -13
  16. package/compiled_src/chatbox/QuoteMessage.js +3 -2
  17. package/compiled_src/chatbox/TextMessage.js +22 -18
  18. package/compiled_src/chatbox/Widget.js +200 -129
  19. package/compiled_src/chatbox/block.js +39 -29
  20. package/compiled_src/chatbox/conversation-history.js +44 -0
  21. package/compiled_src/chatbox/event-layout.js +59 -0
  22. package/compiled_src/chatbox/greeting.js +32 -0
  23. package/compiled_src/common.js +13 -9
  24. package/compiled_src/form/css.js +1 -1
  25. package/compiled_src/lib.js +22 -32
  26. package/compiled_src/notif/css.js +1 -1
  27. package/compiled_src/plugin.js +11 -18
  28. package/compiled_src/popup/css.js +1 -1
  29. package/compiled_src/popup/popup.js +1 -1
  30. package/compiled_src/store/api.js +0 -2
  31. package/compiled_src/store/api_fake.js +1 -1
  32. package/compiled_src/store/attachments.js +28 -0
  33. package/compiled_src/store/chain.js +24 -10
  34. package/compiled_src/store/kv.js +9 -13
  35. package/compiled_src/store/lsdb.js +1 -1
  36. package/compiled_src/store/store.js +356 -117
  37. package/compiled_src/util/common.js +48 -41
  38. package/compiled_src/util/event.js +15 -0
  39. package/compiled_src/util/pubsub.js +28 -13
  40. package/compiled_src/util/url.js +18 -0
  41. package/dist/.HASH +1 -0
  42. package/dist/127.js +13 -13
  43. package/dist/132.js +1 -1
  44. package/dist/1a0ddc53cbb.127.js +1 -0
  45. package/dist/{1a08405c848.132.js → 1a0ddc53cbb.132.js} +1 -1
  46. package/dist/{1a08405c848.27.js → 1a0ddc53cbb.27.js} +1 -1
  47. package/dist/1a0ddc53cbb.278.js +1 -0
  48. package/dist/{1a08405c848.339.js → 1a0ddc53cbb.339.js} +1 -1
  49. package/dist/1a0ddc53cbb.459.js +1 -0
  50. package/dist/1a0ddc53cbb.app.js +1 -0
  51. package/dist/1a0ddc53cbb.chat.html +1 -0
  52. package/dist/1a0ddc53cbb.chat.js +1 -0
  53. package/dist/1a0ddc53cbb.form.html +1 -0
  54. package/dist/1a0ddc53cbb.form.js +1 -0
  55. package/dist/1a0ddc53cbb.index.html +72 -0
  56. package/dist/1a0ddc53cbb.library.html +72 -0
  57. package/dist/1a0ddc53cbb.library.js +1 -0
  58. package/dist/1a0ddc53cbb.preview.html +759 -0
  59. package/dist/1a0ddc53cbb.preview.js +1 -0
  60. package/dist/1a0ddc53cbb.webview.js +1 -0
  61. package/dist/27.js +4 -4
  62. package/dist/459.js +4 -4
  63. package/dist/screenshoot.js +1523 -654
  64. package/package.json +10 -3
  65. package/src/call/css.js +1 -1
  66. package/src/chatbox/Buttons.js +5 -4
  67. package/src/chatbox/ChatButton.js +120 -39
  68. package/src/chatbox/Event.js +38 -5
  69. package/src/chatbox/Events.js +306 -134
  70. package/src/chatbox/FormAttachment.js +1 -1
  71. package/src/chatbox/Header.js +4 -4
  72. package/src/chatbox/InfoMessage.js +1 -1
  73. package/src/chatbox/MessageInput.js +15 -10
  74. package/src/chatbox/OrderAttachments.js +13 -13
  75. package/src/chatbox/QuoteMessage.js +3 -2
  76. package/src/chatbox/TextMessage.js +21 -20
  77. package/src/chatbox/Widget.js +150 -150
  78. package/src/chatbox/block.js +29 -27
  79. package/src/chatbox/conversation-history.js +37 -0
  80. package/src/chatbox/event-layout.js +72 -0
  81. package/src/chatbox/greeting.js +13 -0
  82. package/src/common.js +15 -9
  83. package/src/form/css.js +1 -1
  84. package/src/lib.js +22 -35
  85. package/src/notif/css.js +1 -1
  86. package/src/plugin.js +11 -13
  87. package/src/popup/css.js +1 -1
  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/webpack.prod.js +3 -0
  102. package/assets/less/contact.less +0 -126
  103. package/compiled_src/util/common.test.js +0 -414
  104. package/compiled_src/util/event.test.js +0 -38
  105. package/compiled_src/util/extractor.test.js +0 -299
  106. package/compiled_src/util/extractor.test2.js +0 -61
  107. package/compiled_src/util/gate.test.js +0 -18
  108. package/compiled_src/util/pubsub.test.js +0 -35
  109. package/dist/1a08405c848.127.js +0 -1
  110. package/dist/1a08405c848.278.js +0 -1
  111. package/dist/1a08405c848.459.js +0 -1
  112. package/dist/1a08405c848.74.js +0 -1
  113. package/dist/1a08405c848.997.js +0 -1
  114. package/dist/1a08405c848.app.js +0 -1
  115. package/dist/1a08405c848.chat.html +0 -1
  116. package/dist/1a08405c848.chat.js +0 -1
  117. package/dist/1a08405c848.form.html +0 -1
  118. package/dist/1a08405c848.form.js +0 -1
  119. package/dist/1a08405c848.index.html +0 -1
  120. package/dist/1a08405c848.library.html +0 -1
  121. package/dist/1a08405c848.library.js +0 -1
  122. package/dist/1a08405c848.preview.html +0 -1
  123. package/dist/1a08405c848.preview.js +0 -1
  124. package/dist/1a08405c848.webview.js +0 -1
  125. package/dist/74.js +0 -376
  126. package/dist/997.js +0 -37
  127. package/dist/HASH +0 -1
  128. package/src/contact/contact.js +0 -400
  129. package/src/contact/css.js +0 -1
  130. package/src/util/common.test.js +0 -301
  131. package/src/util/event.test.js +0 -31
  132. package/src/util/extractor.test.js +0 -240
  133. package/src/util/extractor.test2.js +0 -56
  134. package/src/util/gate.test.js +0 -20
  135. package/src/util/pubsub.test.js +0 -37
  136. /package/dist/{1a08405c848.185.js → 1a0ddc53cbb.185.js} +0 -0
  137. /package/dist/{1a08405c848.490.js → 1a0ddc53cbb.490.js} +0 -0
@@ -6,6 +6,7 @@ import Chain from './chain.js'
6
6
  import KV from './kv.js'
7
7
  import APIFake from './api_fake.js'
8
8
  import RealtimeFake from './realtime_fake.js'
9
+ import {uploadAttachments} from './attachments.js'
9
10
  var Realtime = require('@subiz/wsclient')
10
11
  var cookie = require('cookie_js')
11
12
  cookie.defaults.path = '/'
@@ -15,8 +16,6 @@ let memlocalStorage = require('./inmem_localstorage.js')
15
16
  const MAX_CONVO = 20
16
17
  const page_loaded = sb.Date.now()
17
18
 
18
- const DEBUG_ACCOUNT_IDS = ['acqsulrowbxiugvginhw', 'acryguzgwxmamhzmqlyx'] // acc test and minvoice
19
-
20
19
  // available localstorage key mask, user, hasManuallyClosed, isopen, close_preview_at, lastview, bot_run_logs, plugin_run_logs
21
20
  export default class Store {
22
21
  inmem = false
@@ -27,7 +26,6 @@ export default class Store {
27
26
  this.CONNECTION_CHECK_INTERVAL = 3000
28
27
  this.reconnectTimestamp = 0
29
28
  this.g_sync = {}
30
- this.agent_rt_message_sent_dict = {}
31
29
  this._die = false
32
30
  this.forceDevice = null
33
31
  this.forcePlugins = {}
@@ -67,15 +65,22 @@ export default class Store {
67
65
  // dont dupplicate seen
68
66
  this.g_haveSentSeenM = {}
69
67
  this.last_focused = this.now() // 0 -> in focus, timestamp -> lost focus at
70
- window.addEventListener('blur', () => {
68
+ this._onBlur = () => {
69
+ if (this._die) return
71
70
  this.last_focused = this.now()
72
71
  this.sendPresence()
73
- })
74
- window.addEventListener('focus', () => {
72
+ }
73
+ this._onFocus = () => {
74
+ if (this._die) return
75
75
  this.last_focused = 0 //
76
76
  this.sendPresence()
77
- })
78
- window.addEventListener('beforeunload', () => this.api.sendLostFocus())
77
+ }
78
+ this._onBeforeUnload = () => {
79
+ if (!this._die) this.api.sendLostFocus()
80
+ }
81
+ window.addEventListener('blur', this._onBlur)
82
+ window.addEventListener('focus', this._onFocus)
83
+ window.addEventListener('beforeunload', this._onBeforeUnload)
79
84
  this._lastPresenceURL = window.location.href
80
85
  this._urlCheckInterval = sb.setInterval(() => {
81
86
  if (this._die) return sb.clearInterval(this._urlCheckInterval)
@@ -110,7 +115,7 @@ export default class Store {
110
115
  this.sendPresence()
111
116
  }, 60000) // dont bother delete this
112
117
 
113
- onVisibilityChange((val) => {
118
+ this._visibilityCleanup = onVisibilityChange((val) => {
114
119
  if (this._die) return
115
120
  this.browserVisibility = val
116
121
  this.sendPresence()
@@ -120,12 +125,13 @@ export default class Store {
120
125
  this.device = this.getDeviceInfo()
121
126
 
122
127
  // try to get Google Analytics tracking id and client id by periodically checking it after an interval
123
- const _gaInterval = sb.setInterval(() => {
128
+ this._gaInterval = sb.setInterval(() => {
129
+ if (this._die) return
124
130
  let user = this.cache.match('user') || {}
125
131
  if (!user.id) return // store not init
126
132
  var dev = this.getDeviceInfo()
127
133
  if (!dev.ga_tracking_ids || !dev.ga_client_id) return
128
- sb.clearInterval(_gaInterval)
134
+ sb.clearInterval(this._gaInterval)
129
135
  this.device = dev
130
136
 
131
137
  // update gclient id
@@ -154,9 +160,30 @@ export default class Store {
154
160
  console.log('publish connectionStatus', this.connectionStatus)
155
161
  this.pubsub.publish('connection')
156
162
  }
163
+ if (this.connectionStatus === 'active' && (this._needsMessageSync || currentStatus !== 'active')) {
164
+ this._needsMessageSync = false
165
+ this.syncKnownConvoEvents()
166
+ }
157
167
  }
158
168
  }
159
169
 
170
+ syncKnownConvoEvents() {
171
+ if (this._die) return Promise.resolve()
172
+ if (this.knownConvoSyncRequest) return this.knownConvoSyncRequest
173
+ const chain = this.chain
174
+ const request = (async () => {
175
+ const convos = sb.filter(this.cache.all(), (value, key) => key.startsWith('convo_'))
176
+ for (const convo of convos) {
177
+ if (this._die || chain !== this.chain) break
178
+ await this.syncConvoEvents(convo.id)
179
+ }
180
+ })().finally(() => {
181
+ if (this.knownConvoSyncRequest === request) this.knownConvoSyncRequest = null
182
+ })
183
+ this.knownConvoSyncRequest = request
184
+ return request
185
+ }
186
+
160
187
  getConnectionStatus = () => this.connectionStatus
161
188
 
162
189
  // inmem
@@ -342,7 +369,13 @@ export default class Store {
342
369
  async _touchAcc() {
343
370
  if (this.isInsync('account')) return
344
371
  await this.readyGate.entry()
345
- if (!this.isInsync('account')) this.reconnect()
372
+ if (!this.isInsync('account')) this.refreshAccount()
373
+ }
374
+
375
+ refreshAccount() {
376
+ if (this._die || sb.Date.now() < (this.accountRefreshAfter || 0)) return
377
+ this.accountRefreshAfter = sb.Date.now() + 30_000
378
+ return this.reconnect()
346
379
  }
347
380
 
348
381
  // must ensure only call this function after init is done
@@ -426,13 +459,18 @@ export default class Store {
426
459
  await this.readyGate.entry()
427
460
  await this.reconnectGate.entry(undefined, true)
428
461
 
429
- // do nothing when account is synced
430
- if (this.isInsync(cid)) return this.reconnectGate.open()
431
- this.markSync(cid)
432
-
433
- let {body: convo, error} = await this.api.getConversation(cid)
434
- if (convo && !error) this._setConvo(convo)
435
- return this.reconnectGate.open()
462
+ try {
463
+ if (this._die || this.isInsync(cid)) return
464
+ this.convoFetchAfter = this.convoFetchAfter || {}
465
+ if (sb.Date.now() < (this.convoFetchAfter[cid] || 0)) return
466
+ this.convoFetchAfter[cid] = sb.Date.now() + 15_000
467
+ let {body: convo, error} = await this.api.getConversation(cid)
468
+ if (convo && !error) this._setConvo(convo)
469
+ } catch (error) {
470
+ return {error}
471
+ } finally {
472
+ this.reconnectGate.open()
473
+ }
436
474
  }
437
475
 
438
476
  list_provinces = () => this.api.list_provinces()
@@ -447,28 +485,111 @@ export default class Store {
447
485
  return this.chain.listM(id)
448
486
  }
449
487
 
450
- fetchMoreEvents = async (convoid) => {
451
- if (!convoid) return
452
- if (convoid.startsWith('cslight')) return [[], '']
453
- if (!this.isInsync('events' + convoid)) {
454
- await this.readyGate.entry()
455
- await this.reconnectGate.entry(undefined, true)
456
- if (this.isInsync('events' + convoid)) {
457
- this.reconnectGate.open()
458
- return {}
459
- }
460
- this.markSync('events' + convoid)
488
+ fetchMoreEvents = (cid, limit = 150) => {
489
+ this.historyRequests = this.historyRequests || new Map()
490
+ if (this.historyRequests.has(cid)) return this.historyRequests.get(cid)
491
+ this.historyFailures = this.historyFailures || {}
492
+ const failure = this.historyFailures[cid]
493
+ if (failure && sb.Date.now() < failure.retryAt) return Promise.resolve({error: failure.error})
494
+ const request = this._fetchMoreEvents(cid, limit).then(result => {
495
+ if (result.error) this.historyFailures[cid] = {error: result.error, retryAt: sb.Date.now() + 15_000}
496
+ else delete this.historyFailures[cid]
497
+ return result
498
+ }).finally(() => {
499
+ if (this.historyRequests.get(cid) === request) this.historyRequests.delete(cid)
500
+ })
501
+ this.historyRequests.set(cid, request)
502
+ return request
503
+ }
461
504
 
462
- // reset anchor
463
- await this.chain.reload(convoid, () => {
464
- return [[], '']
465
- })
466
- let out = await this._doFetchMsg(convoid)
467
- this.reconnectGate.open()
505
+ async _fetchMoreEvents(convoid, limit = 150) {
506
+ if (!convoid || convoid.startsWith('cslight')) return {}
507
+ await this.readyGate.entry()
508
+ await this.reconnectGate.entry(undefined, true)
509
+ try {
510
+ if (this._die) return dieErr
511
+ const refresh = !this.isInsync('events' + convoid)
512
+ if (refresh) {
513
+ this.chain.anchorCache[convoid] = ''
514
+ delete this.outofmsgM[convoid]
515
+ }
516
+ const out = await this._doFetchMsg(convoid, limit)
517
+ if (!out.error) this.markSync('events' + convoid)
468
518
  return out
519
+ } catch (error) {
520
+ return {error}
521
+ } finally {
522
+ this.reconnectGate.open()
469
523
  }
524
+ }
525
+
526
+ // Fetch from the newest page independently of the older-history cursor.
527
+ // A realtime arrival must not advance this watermark: it could hide a gap.
528
+ syncConvoEvents = (cid) => {
529
+ if (this._die || !cid || cid.startsWith('cslight')) return Promise.resolve({})
530
+ this.messageSyncRequests = this.messageSyncRequests || new Map()
531
+ if (this.messageSyncRequests.has(cid)) return this.messageSyncRequests.get(cid)
532
+ this.messageSyncNextAt = this.messageSyncNextAt || {}
533
+ if (sb.Date.now() < (this.messageSyncNextAt[cid] || 0)) return Promise.resolve({skipped: true})
534
+ const chain = this.chain
535
+ const request = this._syncConvoEvents(cid, chain).then(result => {
536
+ if (chain === this.chain && !this._die) {
537
+ this.messageSyncFailures = this.messageSyncFailures || {}
538
+ const failures = result.error ? Math.min((this.messageSyncFailures[cid] || 0) + 1, 4) : 0
539
+ this.messageSyncFailures[cid] = failures
540
+ this.messageSyncNextAt[cid] = sb.Date.now() + (failures ? 15_000 * 2 ** (failures - 1) : 15_000)
541
+ }
542
+ return result
543
+ }).finally(() => {
544
+ if (this.messageSyncRequests.get(cid) === request) this.messageSyncRequests.delete(cid)
545
+ })
546
+ this.messageSyncRequests.set(cid, request)
547
+ return request
548
+ }
470
549
 
471
- return this._doFetchMsg(convoid)
550
+ async _syncConvoEvents(cid, chain) {
551
+ await this.readyGate.entry()
552
+ await this.reconnectGate.entry(undefined, true)
553
+ try {
554
+ if (this._die || chain !== this.chain) return dieErr
555
+ this.messageSyncTimes = this.messageSyncTimes || {}
556
+ this.messageSyncProgress = this.messageSyncProgress || {}
557
+ let progress = this.messageSyncProgress[cid]
558
+ if (!progress) {
559
+ const cached = Object.values(chain.listM(cid))
560
+ const boundary = this.messageSyncTimes[cid] || cached.reduce((min, ev) => Math.min(min, sb.getMs(ev.created)), Infinity)
561
+ progress = this.messageSyncProgress[cid] = {boundary, anchor: '', newest: 0, visited: new Set()}
562
+ }
563
+ // Bound each batch; the next trigger resumes at the saved cursor.
564
+ for (let page = 0; page < 5; page++) {
565
+ const wait = (this.nextMessageSyncRequestAt || 0) - sb.Date.now()
566
+ if (wait > 0) await sb.sleep(wait)
567
+ if (this._die || chain !== this.chain) return dieErr
568
+ this.nextMessageSyncRequestAt = sb.Date.now() + 250
569
+ const [items, next, error, end] = await this.listmessageapi(cid, progress.anchor, 150)
570
+ if (this._die || chain !== this.chain) return dieErr
571
+ if (error) return {error}
572
+ chain.merge(cid, items)
573
+ for (const item of items) progress.newest = Math.max(progress.newest, item.index)
574
+ this.pubsub.publish('account')
575
+ if (end || !items.length || items.some(item => item.index < progress.boundary)) {
576
+ if (progress.newest) this.messageSyncTimes[cid] = progress.newest
577
+ delete this.messageSyncProgress[cid]
578
+ return {}
579
+ }
580
+ progress.visited.add(progress.anchor)
581
+ if (!next || progress.visited.has(next)) {
582
+ delete this.messageSyncProgress[cid]
583
+ return {error: 'message_cursor_stalled'}
584
+ }
585
+ progress.anchor = next
586
+ }
587
+ return {pending: true}
588
+ } catch (error) {
589
+ return {error}
590
+ } finally {
591
+ this.reconnectGate.open()
592
+ }
472
593
  }
473
594
 
474
595
  reloadConvoEvents = async (convoid) => {
@@ -483,8 +604,8 @@ export default class Store {
483
604
  return {error}
484
605
  }
485
606
 
486
- _doFetchMsg = async (convoid) => {
487
- let {error} = await this.chain.fetchMore(convoid, 150, async (type, anchor, limit) => {
607
+ _doFetchMsg = async (convoid, limit = 150) => {
608
+ let {error} = await this.chain.fetchMore(convoid, limit, async (type, anchor, limit) => {
488
609
  if (this.outofmsgM[convoid]) return [[], '']
489
610
  let [events, newanchor, error, outofmsg] = await this.listmessageapi(type, anchor, limit)
490
611
  if (outofmsg) this.outofmsgM[convoid] = true
@@ -621,7 +742,6 @@ export default class Store {
621
742
 
622
743
  async _initResource(accid, auth, realtimep) {
623
744
  this.g_sync = {}
624
- this._current_acc_id = accid
625
745
 
626
746
  let user = await this.api.setAuth(auth)
627
747
  if (typeof sbz_info != 'undefined') sbz_info.store_7 = true
@@ -635,6 +755,7 @@ export default class Store {
635
755
  let lightConvos = this.kv.match('light_convos') || {}
636
756
  this.lightConvos = {}
637
757
  sb.map(lightConvos, (convo) => {
758
+ if (convo.light_type === 'widget' && (convo.account_id !== accid || convo.initial_by !== user.id)) return
638
759
  let in7Day = convo.created > sb.Date.now() - 7 * 24 * 3_600_000
639
760
  let events = sb.filter(convo.events, (ev) => ev.type === 'message_sent')
640
761
 
@@ -652,8 +773,10 @@ export default class Store {
652
773
 
653
774
  this.realtime.onEvent(this.onWs.bind(this))
654
775
  this.realtime.onInterrupted(async () => {
776
+ if (this._die) return
777
+ this._needsMessageSync = true
655
778
  await this.reconnectWs(accid, user.id)
656
- this.reconnect()
779
+ await this.refreshAccount()
657
780
  })
658
781
 
659
782
  if (typeof sbz_info != 'undefined') sbz_info.store_8 = true
@@ -709,7 +832,7 @@ export default class Store {
709
832
  if (this.g_haveSentSeenM[lastmsgsent.id]) return
710
833
 
711
834
  // ignore sending message
712
- if (lastmsgsent.id === lastmsgsent.data.message.idempotency_key) return
835
+ if (lastmsgsent.id === lastmsgsent.source_event_id) return
713
836
 
714
837
  // fire own event so guest code can update unread number
715
838
  this.pubsub.publish('events', {
@@ -765,40 +888,53 @@ export default class Store {
765
888
  await this.readyGate.entry()
766
889
  }
767
890
 
768
- async reconnect(p) {
891
+ reconnect(p) {
892
+ if (this.reconnectRequest) return this.reconnectRequest
893
+ const request = this._reconnect(p).finally(() => {
894
+ if (this.reconnectRequest === request) this.reconnectRequest = null
895
+ })
896
+ this.reconnectRequest = request
897
+ return request
898
+ }
899
+
900
+ async _reconnect(p) {
769
901
  if (this._die) return
770
902
  // make sure we only run reconnect once in a time
771
903
  await this.reconnectGate.entry(undefined, true)
772
- // do nothing when account is synced
773
- // if (this.isInsync('account')) return this.reconnectGate.open()
774
- this.markSync('account')
775
-
776
- let fetchuser = p && p.user ? Promise.resolve({body: p.user}) : this.api.getUser()
777
- let outs = await Promise.all([fetchuser, this.api.getAccountSetting({bot_id: this.forceBotId})])
778
-
779
- let error = sb.find(outs, (out) => out.error)
780
- if (error) return console.error(error)
781
-
782
- let set = outs[1].body
783
- set.user = outs[0].body
784
- await this._setSetting(set)
785
- // refetch convo
786
- let convob = await this.api.listConversations()
787
-
788
- error = convob.error
789
- let body = convob.body
790
- if (!error) {
791
- let convos = sb.get(body, 'conversations', [])
792
- // clear all old convo from two week
793
- sb.map(this.cache.all(), (value, k) => {
794
- if (k.startsWith('convo_')) {
795
- if (this.now() - sb.getMs(sb.get(value, 'actived')) > 1209600000) this.cache.del(k)
796
- }
797
- })
798
- sb.map(convos, (convo) => this._setConvo(convo))
799
- }
904
+ try {
905
+
906
+ let fetchuser = p && p.user ? Promise.resolve({body: p.user}) : this.api.getUser()
907
+ let outs = await Promise.all([fetchuser, this.api.getAccountSetting({bot_id: this.forceBotId})])
908
+
909
+ let error = sb.find(outs, (out) => out.error)
910
+ if (error) return error
911
+
912
+ let set = outs[1].body
913
+ set.user = outs[0].body
914
+ await this._setSetting(set)
915
+ // refetch convo
916
+ let convob = await this.api.listConversations()
917
+
918
+ error = convob.error
919
+ let body = convob.body
920
+ if (!error) {
921
+ let convos = sb.get(body, 'conversations', [])
922
+ // clear all old convo from two week
923
+ sb.map(this.cache.all(), (value, k) => {
924
+ if (k.startsWith('convo_')) {
925
+ if (this.now() - sb.getMs(sb.get(value, 'actived')) > 1209600000) this.cache.del(k)
926
+ }
927
+ })
928
+ sb.map(convos, (convo) => this._setConvo(convo))
929
+ }
800
930
 
801
- return this.reconnectGate.open()
931
+ if (error) return {error}
932
+ this.markSync('account')
933
+ } catch (error) {
934
+ return {error}
935
+ } finally {
936
+ this.reconnectGate.open()
937
+ }
802
938
  }
803
939
 
804
940
  async submitForm(formid, data, user) {
@@ -949,39 +1085,18 @@ export default class Store {
949
1085
  matchTrafficSource = () => this.kv.match('traffic_source')
950
1086
 
951
1087
  async onWs(ev) {
952
- await this.wsGate.entry()
953
- this.wsGate.close()
954
- await this._onWs(ev)
955
- this.wsGate.open()
956
- }
957
-
958
- sendBugReport = (str) => this.api.sendBugReport(str)
959
-
960
- matchDebugAccountIds = () => DEBUG_ACCOUNT_IDS
961
-
962
- handleLogCompare(ev) {
963
- if (!DEBUG_ACCOUNT_IDS.includes(this._current_acc_id)) return
964
- if (ev.type !== 'message_sent') return
965
- let byType = sb.get(ev, 'by.type')
966
- if (byType === 'user') return
967
-
968
- this.agent_rt_message_sent_dict[ev.id] = ev
969
- }
970
-
971
- sbzListConversations = () => {
972
- return this.api.listConversations()
973
- }
974
-
975
- sbzListMessages = (cid) => {
976
- return this.api.listMessages(cid, 100, '')
977
- }
978
-
979
- listAgentRealtimeMessageSent = () => {
980
- return this.agent_rt_message_sent_dict
1088
+ await this.wsGate.entry(undefined, true)
1089
+ try {
1090
+ await this._onWs(ev)
1091
+ } catch (error) {
1092
+ console.error(error)
1093
+ return {error}
1094
+ } finally {
1095
+ this.wsGate.open()
1096
+ }
981
1097
  }
982
1098
 
983
1099
  async _onWs(ev) {
984
- this.handleLogCompare(ev)
985
1100
  if (this._die) return
986
1101
  if (!ev || !ev.type) return
987
1102
  ev.created = sb.getMs(ev.created)
@@ -1063,7 +1178,7 @@ export default class Store {
1063
1178
  }
1064
1179
 
1065
1180
  if (sb.startsWith(ev.type, 'message_') || sb.startsWith(ev.type, 'conversation_')) {
1066
- this.restConvoEvent(ev)
1181
+ await this.restConvoEvent(ev)
1067
1182
  if (ev.type === 'message_sent' && sb.get(ev, 'by.id') !== user.id) {
1068
1183
  sendEventToThirdParty('SUBIZ Chat', 'Received', 'User')
1069
1184
  }
@@ -1116,8 +1231,8 @@ export default class Store {
1116
1231
  // dont store pong and typing
1117
1232
  if (ev.type !== 'message_pong' && ev.type !== 'conversation_typing') {
1118
1233
  if (ev.type === 'message_sent') {
1119
- let idemkey = sb.get(ev, 'data.message.idempotency_key')
1120
- let found = sb.find(this.listEvents(cid), (oldev) => oldev.id === idemkey)
1234
+ let sourceEventId = sb.get(ev, 'source_event_id')
1235
+ let found = sb.find(this.listEvents(cid), (oldev) => oldev.id === sourceEventId)
1121
1236
  if (found) {
1122
1237
  // keeping old created so the ui can be static
1123
1238
  this.chain.del(cid, found.id)
@@ -1149,6 +1264,39 @@ export default class Store {
1149
1264
  this.kv.put('light_convos', this.lightConvos)
1150
1265
  }
1151
1266
 
1267
+ startWidgetConvo(greeting, locale) {
1268
+ const user = this.matchUser()
1269
+ const existing = sb.find(this.lightConvos, convo => convo.light_type === 'widget' && convo.account_id === user.account_id && convo.initial_by === user.id)
1270
+ if (existing) return {convo: existing}
1271
+ const id = 'cslightwidget_' + sb.randomString(24)
1272
+ const ev = sb.cloneDeep(greeting)
1273
+ ev.data.message.conversation_id = id
1274
+ const convo = {
1275
+ id, account_id: user.account_id, initial_by: user.id, locale,
1276
+ light_type: 'widget', state: 'pending', created: ev.created,
1277
+ members: [{id: user.id, type: 'user', seen_at: ev.created + 1}],
1278
+ events: [ev], last_message_sent: ev,
1279
+ touchpoint: {id, channel: 'subiz', source: 'web'},
1280
+ }
1281
+ this.lightConvos[id] = convo
1282
+ this.kv.put('light_convos', this.lightConvos)
1283
+ this.pubsub.publish('account')
1284
+ return {convo}
1285
+ }
1286
+
1287
+ promoteWidgetConvo(localId, cid) {
1288
+ // Keep the mounted timeline and its message keys across draft promotion.
1289
+ this.widgetConvoOrigins = this.widgetConvoOrigins || {}
1290
+ this.widgetConvoOrigins[cid] = localId
1291
+ const greeting = sb.cloneDeep(sb.get(this.lightConvos, [localId, 'events', 0]))
1292
+ if (greeting) {
1293
+ greeting.source_event_id = greeting.id
1294
+ greeting.data.message.conversation_id = cid
1295
+ this.chain.merge(cid, [{id: greeting.id, index: greeting.created, value: greeting}])
1296
+ }
1297
+ this.deleteLightConvo(localId)
1298
+ }
1299
+
1152
1300
  startLightConvo(botid, messages, locale, is_testing, botversion) {
1153
1301
  let convoid = 'cslight' + botid + '_' + this.now()
1154
1302
  let user = this.cache.match('user') || {}
@@ -1229,11 +1377,11 @@ export default class Store {
1229
1377
 
1230
1378
  async sendConvoEvent(cid, type, data) {
1231
1379
  await this.readyGate.entry()
1232
- var idempotency = sb.randomString(36)
1380
+ var sourceEventId = sb.randomString(36)
1233
1381
  let user = this.cache.match('user') || {}
1234
1382
 
1235
1383
  var event = {
1236
- id: idempotency,
1384
+ id: sourceEventId,
1237
1385
  by: {id: user.id, type: 'user', device: this.device},
1238
1386
  data,
1239
1387
  type,
@@ -1242,7 +1390,7 @@ export default class Store {
1242
1390
  sb.convoToEv(cid, event)
1243
1391
  let files = []
1244
1392
  if (type === 'message_sent') {
1245
- event.data.message.idempotency_key = idempotency
1393
+ event.source_event_id = sourceEventId
1246
1394
  event.data.message.conversation_id = cid
1247
1395
 
1248
1396
  // pre upload image files
@@ -1263,14 +1411,9 @@ export default class Store {
1263
1411
  await this.convoGate.entry()
1264
1412
  // upload file first
1265
1413
 
1266
- await flow.map(files, 1, async (att) => {
1267
- const {body, url, error} = await this.api.uploadToS3(att._file)
1268
- if (sb.get(body, 'code') == 'invalid_field_size') {
1269
- event._error_message = 'cannot_send_too_big_file'
1270
- }
1271
- event._error = error
1272
- att.url = url
1273
- })
1414
+ const uploaded = await uploadAttachments(files, (file) => this.api.uploadToS3(file))
1415
+ event._error = uploaded.error
1416
+ if (uploaded.errorMessage) event._error_message = uploaded.errorMessage
1274
1417
 
1275
1418
  if (event._error) return this.onWs(event) // dont send error msg
1276
1419
  if (cid && cid.startsWith('cslight')) return
@@ -1413,9 +1556,12 @@ export default class Store {
1413
1556
 
1414
1557
  async listmessageapi(type, anchor, limit) {
1415
1558
  let convoid = type
1416
- if (convoid && convoid.startsWith('cslight')) return [fakeevents, '', undefined, true]
1559
+ if (convoid && convoid.startsWith('cslight')) {
1560
+ let events = sb.map(this.listEvents(convoid), (ev) => ({id: ev.id, index: ev.created, value: ev}))
1561
+ return [events, '', undefined, true]
1562
+ }
1417
1563
  let {body: events, code, error} = await this.api.listMessages(convoid, -limit, anchor)
1418
- if (error || code !== 200) return [[], anchor, error]
1564
+ if (error || code !== 200) return [[], anchor, error || 'message_fetch_failed']
1419
1565
  anchor = sb.get(sb.last(events), 'id')
1420
1566
 
1421
1567
  events = sb.map(events, (event) => {
@@ -1450,15 +1596,17 @@ export default class Store {
1450
1596
  let startms = this.now()
1451
1597
 
1452
1598
  // store not init
1453
- if (!this.cache || !sb.get(this.cache.match('user'), 'id')) return sb.setTimeout(() => this.loopTime(), 240000)
1599
+ if (!this.cache || !sb.get(this.cache.match('user'), 'id'))
1600
+ return (this._timeTimeout = sb.setTimeout(() => this.loopTime(), 240000))
1454
1601
 
1455
1602
  let {body: t, error} = await this.api.time()
1456
- if (error) return sb.setTimeout(() => this.loopTime(), 30000)
1603
+ if (this._die) return
1604
+ if (error) return (this._timeTimeout = sb.setTimeout(() => this.loopTime(), 30000))
1457
1605
  var latences = Math.abs(this.now() - startms) / 2
1458
1606
  var serverMs = new sb.Date(t).getTime() + latences
1459
1607
  var localMs = this.now()
1460
1608
  this.timeDiff = Math.floor(serverMs - localMs)
1461
- sb.setTimeout(() => this.loopTime(), 240000)
1609
+ this._timeTimeout = sb.setTimeout(() => this.loopTime(), 240000)
1462
1610
  }
1463
1611
 
1464
1612
  replaceFileUrl(src) {
@@ -1497,6 +1645,14 @@ export default class Store {
1497
1645
  this.kv = new KV(new LSDB('sbz2023', this.inmem), 'kv')
1498
1646
  if (this.chain) this.chain.destroy()
1499
1647
  this.chain = new Chain(new LSDB('cbz2023', true), 'ch')
1648
+ this.outofmsgM = {}
1649
+ this.messageSyncTimes = {}
1650
+ this.messageSyncNextAt = {}
1651
+ this.messageSyncFailures = {}
1652
+ this.messageSyncProgress = {}
1653
+ this.accountRefreshAfter = 0
1654
+ this.historyFailures = {}
1655
+ this.convoFetchAfter = {}
1500
1656
  if (this.realtime) this.realtime.stop()
1501
1657
  if (this.cache) this.cache.destroy()
1502
1658
  this.pubsub.publish('destroy')
@@ -1506,14 +1662,22 @@ export default class Store {
1506
1662
  return this._die
1507
1663
  }
1508
1664
  destroy() {
1665
+ if (this._die) return
1509
1666
  this._die = true
1510
1667
  this.clearBrowserData()
1511
1668
  sb.clearInterval(this._seenInterval)
1512
1669
  sb.clearInterval(this._urlCheckInterval)
1670
+ sb.clearInterval(this._presenceInterval1)
1671
+ sb.clearInterval(this._presenceInterval2)
1672
+ sb.clearInterval(this._gaInterval)
1673
+ sb.clearTimeout(this._timeTimeout)
1513
1674
  if (this.realtime) this.realtime.stop()
1514
- this.readyGate.close()
1515
- window.removeEventListener('blur', this.sendPresence)
1516
- window.removeEventListener('focus', this.sendPresence)
1675
+ if (this.readyGate) this.readyGate.close()
1676
+ window.removeEventListener('blur', this._onBlur)
1677
+ window.removeEventListener('focus', this._onFocus)
1678
+ window.removeEventListener('beforeunload', this._onBeforeUnload)
1679
+ if (this._visibilityCleanup) this._visibilityCleanup()
1680
+ this.pubsub.clear()
1517
1681
  }
1518
1682
  }
1519
1683
 
@@ -1543,16 +1707,29 @@ const ImageTypes = ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml']
1543
1707
 
1544
1708
  function onVisibilityChange(cb) {
1545
1709
  var hidden = 'hidden'
1710
+ let target = document,
1711
+ eventName
1712
+ let previous = {}
1546
1713
 
1547
1714
  // Standards:
1548
- if (hidden in document) document.addEventListener('visibilitychange', onchange)
1549
- else if ((hidden = 'mozHidden') in document) document.addEventListener('mozvisibilitychange', onchange)
1550
- else if ((hidden = 'webkitHidden') in document) document.addEventListener('webkitvisibilitychange', onchange)
1551
- else if ((hidden = 'msHidden') in document) document.addEventListener('msvisibilitychange', onchange)
1715
+ if (hidden in document) eventName = 'visibilitychange'
1716
+ else if ((hidden = 'mozHidden') in document) eventName = 'mozvisibilitychange'
1717
+ else if ((hidden = 'webkitHidden') in document) eventName = 'webkitvisibilitychange'
1718
+ else if ((hidden = 'msHidden') in document) eventName = 'msvisibilitychange'
1552
1719
  // IE 9 and lower:
1553
- else if ('onfocusin' in document) document.onfocusin = document.onfocusout = onchange
1720
+ else if ('onfocusin' in document) {
1721
+ previous = {onfocusin: document.onfocusin, onfocusout: document.onfocusout}
1722
+ document.onfocusin = document.onfocusout = onchange
1723
+ }
1554
1724
  // All others:
1555
- else window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onchange
1725
+ else {
1726
+ target = window
1727
+ for (const key of ['onpageshow', 'onpagehide', 'onfocus', 'onblur']) {
1728
+ previous[key] = window[key]
1729
+ window[key] = onchange
1730
+ }
1731
+ }
1732
+ if (eventName) target.addEventListener(eventName, onchange)
1556
1733
 
1557
1734
  function onchange(evt) {
1558
1735
  var v = 'visible'
@@ -1566,6 +1743,12 @@ function onVisibilityChange(cb) {
1566
1743
 
1567
1744
  // set the initial state (but only if browser supports the Page Visibility API)
1568
1745
  if (document[hidden] !== undefined) onchange({type: document[hidden] ? 'blur' : 'focus'})
1746
+ return () => {
1747
+ if (eventName) target.removeEventListener(eventName, onchange)
1748
+ for (const key of Object.keys(previous)) {
1749
+ if (target[key] === onchange) target[key] = previous[key]
1750
+ }
1751
+ }
1569
1752
  }
1570
1753
 
1571
1754
  const inmemCookie = {