@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
@@ -1,3 +1,6 @@
1
+ import {processEvents} from './event-layout.js'
2
+ import {conversationHistory, conversationEvents, conversationEventKey} from './conversation-history.js'
3
+ import {widgetGreeting} from './greeting.js'
1
4
  import Event from './Event.js'
2
5
  import PoweredBy from './PoweredBy.js'
3
6
  const convoemptyimg = require('../../assets/img/convos_empty.svg')
@@ -31,6 +34,11 @@ export default class Events extends Com {
31
34
  }
32
35
 
33
36
  componentDidMount() {
37
+ this.syncTimer = sb.setInterval(() => this.syncMessages(), 120_000)
38
+ this.resumeSync = () => this.syncMessages()
39
+ window.addEventListener('focus', this.resumeSync)
40
+ document.addEventListener('visibilitychange', this.resumeSync)
41
+ this.syncMessages()
34
42
  this.setState({
35
43
  isShowTyping: false,
36
44
  typing_event: {},
@@ -67,14 +75,127 @@ export default class Events extends Com {
67
75
  })
68
76
 
69
77
  this.props.store.onMessageUpdated(this, async (ev) => {
70
- if (sb.convoOfEv(ev) !== this.props.cid) return
71
- await this.props.store.reloadConvoEvents(this.props.cid)
78
+ const cid = sb.convoOfEv(ev)
79
+ if (!this.visibleCounts || !this.visibleCounts[cid]) return
80
+ await this.props.store.reloadConvoEvents(cid)
72
81
  this.forceUpdate()
73
82
  })
83
+ this.restoreInitialBottom()
84
+ this.observeHistoryStart()
74
85
  }
75
86
 
76
87
  componentDidUpdate(prevProps) {
77
- if (prevProps.cid !== this.props.cid) this.loadConvo()
88
+ if (prevProps.cid !== this.props.cid && !this.isDraftPromotion(prevProps, this.props)) this.loadConvo()
89
+ if (prevProps.cid !== this.props.cid || (!prevProps.isShow && this.props.isShow)) this.syncMessages()
90
+ this.restoreInitialBottom()
91
+ this.observeHistoryStart()
92
+ }
93
+
94
+ componentWillReceiveProps(nextProps) {
95
+ if (nextProps.cid === this.props.cid) return
96
+ if (this.isDraftPromotion(this.props, nextProps)) {
97
+ this.visibleCounts[nextProps.cid] = this.visibleCounts[this.props.cid] || 30
98
+ delete this.visibleCounts[this.props.cid]
99
+ return
100
+ }
101
+ // Prepare the new conversation before its first render, not after the DOM update.
102
+ this.loadVersion = (this.loadVersion || 0) + 1
103
+ this.visibleCounts = this.initialVisibleCounts(nextProps)
104
+ this.evRefs = {}
105
+ this.historyBusy = true
106
+ this.setState({error: false, historyError: false, historyLoading: false, isShowTyping: false, loading: nextProps.cid ? sb.Date.now() : false})
107
+ }
108
+
109
+ isDraftPromotion(previous, next) {
110
+ return !!previous.cid && sb.get(next.store.widgetConvoOrigins, next.cid) === previous.cid
111
+ }
112
+
113
+ initialVisibleCounts(props) {
114
+ const counts = props.cid ? {[props.cid]: 30} : {}
115
+ if (!props.store.matchConvo || sb.get(props.store.matchConvo(props.cid), 'light_type') !== 'widget') return counts
116
+ const history = conversationHistory(props.store, props.cid).slice(0, -1).reverse()
117
+ let remaining = 30
118
+ for (const convo of history) {
119
+ const cached = sb.size(props.store.listEvents(convo.id))
120
+ if (!cached || !remaining) break
121
+ counts[convo.id] = Math.min(cached, remaining)
122
+ remaining -= counts[convo.id]
123
+ if (!sb.get(props.store.outofmsgM, convo.id)) break
124
+ }
125
+ return counts
126
+ }
127
+
128
+ syncMessages() {
129
+ if (!this._isMounted || !this.props.isShow || document.visibilityState === 'hidden') return
130
+ const ids = Object.keys(this.visibleCounts || {}).filter(cid => this.visibleCounts[cid])
131
+ return Promise.all(ids.map(cid => this.props.store.syncConvoEvents(cid)))
132
+ }
133
+
134
+ componentWillUnmount() {
135
+ sb.clearInterval(this.syncTimer)
136
+ window.removeEventListener('focus', this.resumeSync)
137
+ document.removeEventListener('visibilitychange', this.resumeSync)
138
+ this.loadVersion = (this.loadVersion || 0) + 1
139
+ if (this.timer) sb.clearTimeout(this.timer)
140
+ if (this.historyCheckTimer) sb.clearTimeout(this.historyCheckTimer)
141
+ if (this.historyObserver) this.historyObserver.disconnect()
142
+ if (this.scrollTimer) sb.clearTimeout(this.scrollTimer)
143
+ if (this.initialSizeObserver) this.initialSizeObserver.disconnect()
144
+ }
145
+
146
+ stopInitialBottom() {
147
+ this.initialBottom = false
148
+ }
149
+
150
+ restoreInitialBottom() {
151
+ if (!this.initialBottom || this.props.isShow === false || !this.refBody) return
152
+ // Restore after DOM commits, and again when images/fonts or the widget size settle.
153
+ // Unlike isBottom(), this records the opening intent before the content grows.
154
+ if (window.ResizeObserver && (this.initialObservedBody !== this.refBody || this.initialObservedContent !== this.refContent)) {
155
+ if (this.initialSizeObserver) this.initialSizeObserver.disconnect()
156
+ this.initialObservedBody = this.refBody
157
+ this.initialObservedContent = this.refContent
158
+ this.initialSizeObserver = new window.ResizeObserver(() => {
159
+ if (this._isMounted) this.restoreInitialBottom()
160
+ })
161
+ this.initialSizeObserver.observe(this.refBody)
162
+ if (this.refContent) this.initialSizeObserver.observe(this.refContent)
163
+ }
164
+ this.refBody.scrollTop = this.refBody.scrollHeight
165
+ if (this.checkAndSeen) this.checkAndSeen()
166
+ }
167
+
168
+ observeHistoryStart() {
169
+ if (this.observedHistoryStart !== this.historyStart || this.observedHistoryRoot !== this.refBody) {
170
+ if (this.historyObserver) this.historyObserver.disconnect()
171
+ this.observedHistoryStart = this.historyStart
172
+ this.observedHistoryRoot = this.refBody
173
+ if (this.historyStart && this.refBody && window.IntersectionObserver) {
174
+ this.historyObserver = new window.IntersectionObserver(entries => {
175
+ if (entries.some(entry => entry.isIntersecting)) this.queueHistoryCheck()
176
+ }, {root: this.refBody})
177
+ this.historyObserver.observe(this.historyStart)
178
+ }
179
+ }
180
+ this.queueHistoryCheck()
181
+ }
182
+
183
+ queueHistoryCheck() {
184
+ if (this.historyCheckTimer || !this._isMounted) return
185
+ this.historyCheckTimer = sb.setTimeout(() => {
186
+ this.historyCheckTimer = null
187
+ this.loadVisibleHistory()
188
+ }, 0)
189
+ }
190
+
191
+ loadVisibleHistory() {
192
+ if (!this._isMounted || this.props.isShow === false || this.historyBusy || this.state.historyError || !this.hasMore()) return
193
+ if (!this.refBody || !this.historyStart) return
194
+ const root = this.refBody.getBoundingClientRect()
195
+ const marker = this.historyStart.getBoundingClientRect()
196
+ if (root.width <= 0 || root.height <= 0 || marker.height <= 0) return
197
+ if (marker.right <= Math.max(root.left, 0) || marker.left >= Math.min(root.right, window.innerWidth)) return
198
+ if (marker.bottom > Math.max(root.top, 0) && marker.top < Math.min(root.bottom, window.innerHeight)) return this.loadMore()
78
199
  }
79
200
 
80
201
  setEventChildRef(ele, id) {
@@ -109,24 +230,28 @@ export default class Events extends Com {
109
230
 
110
231
  render() {
111
232
  if (this.state.error) return this.renderError()
112
- let events = sb.map(this.props.store.listEvents(this.props.cid))
233
+ const conversations = conversationHistory(this.props.store, this.props.cid).filter(convo => this.visibleCounts && this.visibleCounts[convo.id])
234
+ let events = conversations.flatMap(convo => {
235
+ const entries = conversationEvents(this.props.store, convo).slice(-this.visibleCounts[convo.id]).filter(entry => convo.id === this.props.cid || entry.ev.type !== 'conversation_state_updated')
236
+ processEvents(convo, entries)
237
+ return entries.map(entry => ({...entry, convo}))
238
+ })
113
239
 
114
- if (!this.state.loading && sb.size(events) === 0 && !this.props.cid) {
115
- return (
116
- <div ref={(ref) => (this.refBody = ref)} class='widget-body--events-container'>
117
- <div class='widget-body--empty'>
118
- <div class='widget-body--empty-icon'></div>
119
- <div class='widget-body--empty-text'>{this.props.t('empty_convo_text')}</div>
120
- </div>
121
- <PoweredBy store={this.props.store} />
122
- </div>
123
- )
240
+ if (!this.state.loading && !this.props.creating && events.length === 0 && !this.props.cid) {
241
+ if (!this.greetingCreated) this.greetingCreated = sb.Date.now()
242
+ // Presentation only: never insert this greeting into the store or send it.
243
+ events.push({
244
+ mode: 'lead',
245
+ is_last_message_send: true,
246
+ ev: {
247
+ ...widgetGreeting(this.props.chatbox, this.props.t, this.greetingCreated),
248
+ id: 'widget-virtual-greeting',
249
+ },
250
+ })
124
251
  }
125
252
 
126
- events = sb.orderBy(events, 'created', 'asc')
127
-
253
+ this.renderedIds = new Set(events.map(entry => entry.ev.id))
128
254
  let convo = this.props.store.matchConvo(this.props.cid)
129
- events = sb.map(events, (ev) => ({ev: ev}))
130
255
 
131
256
  // console.log('-----------', this.state.isShowTyping, this.state.typing_event)
132
257
  let ev = this.state.typing_event || {id: 'evzz' + sb.Date.now(), created: sb.Date.now(), by: {type: 'agent'}}
@@ -143,14 +268,20 @@ export default class Events extends Com {
143
268
  },
144
269
  }
145
270
 
146
- processEvents(convo, events)
271
+ const hasMessages = events.some(entry => entry.ev.type === 'message_sent')
147
272
  // we dont want fakeev cause some unexpected buf when processEvents
148
273
  events.push(fakeev)
149
274
 
150
275
  let $events = sb.map(events, (ev) => {
151
- let isLast = ev.is_last_message_send && sb.get(convo, 'state') !== 'ended'
276
+ const sourceConvo = ev.convo || convo
277
+ const isHistory = sourceConvo && sourceConvo.id !== this.props.cid
278
+ let isLast = !isHistory && ev.is_last_message_send && sb.get(sourceConvo, 'state') !== 'ended'
279
+ const sourceId = sourceConvo ? sourceConvo.id : ''
280
+ const key = (sb.get(this.props.store.widgetConvoOrigins, sourceId) || sourceId) + ':' + conversationEventKey(ev.ev)
152
281
  return (
282
+ <fieldset class={isHistory ? 'conversation-event conversation-event--history' : 'conversation-event'} disabled={isHistory} data-conversation-id={sourceConvo && sourceConvo.id} key={key}>
153
283
  <Event
284
+ isHistory={isHistory}
154
285
  isLast={isLast}
155
286
  chatbox={this.props.chatbox}
156
287
  onLink={this.props.onLink}
@@ -160,44 +291,60 @@ export default class Events extends Com {
160
291
  onPostback={this.props.onPostback}
161
292
  onFocusSourceEventFromQuote={this.onFocusSourceEventFromQuote}
162
293
  ref={(ele) => this.setEventChildRef(ele, ev.ev.id)}
163
- isConvoEnded={sb.get(convo, 'state') === 'ended'}
294
+ isConvoEnded={sb.get(sourceConvo, 'state') === 'ended'}
164
295
  event={ev}
165
296
  t={this.props.t}
166
- key={ev.ev.id}
297
+ key={key}
167
298
  />
299
+ </fieldset>
168
300
  )
169
301
  })
170
302
 
171
303
  let $loading = null
172
- let style = 'opacity: 1 !important'
173
- if (this.props.creating) {
304
+ if (!hasMessages && (this.props.creating || this.state.loading)) {
174
305
  $loading = <Spinner static mode={sb.get(this.props.chatbox, 'primary_color', 'dark')} size='120' center />
175
- style = 'opacity: 0 !important'
176
- }
177
-
178
- if (this.state.loading) {
179
- if (sb.Date.now() - this.state.loading > 500) {
180
- $loading = <Spinner static mode={sb.get(this.props.chatbox, 'primary_color', 'dark')} size='120' center />
181
- style = 'opacity: 0 !important'
182
- }
183
306
  }
184
307
 
185
308
  return (
186
309
  <Fragment>
187
310
  {$loading}
188
311
  <div
189
- style={style}
190
312
  ref={(ref) => (this.refBody = ref)}
191
313
  class='widget-body--events-container'
192
314
  onMouseOver={this.checkAndSeen}
315
+ onScroll={() => this.queueHistoryCheck()}
316
+ onWheel={() => this.stopInitialBottom()}
317
+ onTouchStart={() => this.stopInitialBottom()}
318
+ onPointerDown={() => this.stopInitialBottom()}
319
+ onKeyDown={() => this.stopInitialBottom()}
193
320
  >
194
- <div class='widget-body--events'>{$events}</div>
321
+ {this.hasMore() && <div ref={ref => {this.historyStart = ref}} class='conversation-history-start' aria-hidden='true' />}
322
+ {this.state.historyLoading && !this.state.historyError && <Spinner size='16' mode='blue' />}
323
+ {this.state.historyError && this.renderHistoryError()}
324
+ <div class='widget-body--events' ref={ref => {this.refContent = ref}}>{$events}</div>
195
325
  <PoweredBy store={this.props.store} />
196
326
  </div>
197
327
  </Fragment>
198
328
  )
199
329
  }
200
330
 
331
+ renderHistoryError() {
332
+ const t = this.props.t
333
+ const translated = t('load_history_error')
334
+ const locale = t.locale ? t.locale().toLowerCase() : 'en'
335
+ const fallback = locale.startsWith('vi') ? 'Chưa tải được tin nhắn cũ' : 'Couldn’t load earlier messages'
336
+ const text = translated && translated !== 'load_history_error' ? translated : fallback
337
+ return (
338
+ <div class='conversation-history-error' role='status'>
339
+ <span class='conversation-history-error__text'>{text}</span>
340
+ <button class='conversation-history-error__retry' type='button' disabled={this.state.historyLoading} aria-label={t('try_again')} aria-busy={!!this.state.historyLoading} onClick={() => this.loadMore()}>
341
+ <span class='conversation-history-error__retry-label' style={{visibility: this.state.historyLoading ? 'hidden' : 'visible'}}>{t('try_again')}</span>
342
+ {this.state.historyLoading && <span class='conversation-history-error__spinner' aria-hidden='true'><Spinner size='14' /></span>}
343
+ </button>
344
+ </div>
345
+ )
346
+ }
347
+
201
348
  renderError() {
202
349
  let $tryAgainBtn = (
203
350
  <a href='#' onclick={(_) => this.loadConvo()}>
@@ -248,42 +395,131 @@ export default class Events extends Com {
248
395
  }
249
396
 
250
397
  onEvent(newev) {
251
- if (sb.convoOfEv(newev) !== this.props.cid) return
398
+ const cid = sb.convoOfEv(newev)
399
+ if (!this.visibleCounts || !this.visibleCounts[cid]) return
400
+ if (newev.type === 'message_sent') {
401
+ const sourceEventId = sb.get(newev, 'source_event_id')
402
+ const key = cid + ':' + (sourceEventId || newev.id)
403
+ this.receivedMessageKeys = this.receivedMessageKeys || new Set()
404
+ const known = this.receivedMessageKeys.has(key) || (this.renderedIds && (this.renderedIds.has(newev.id) || this.renderedIds.has(sourceEventId)))
405
+ this.receivedMessageKeys.add(key)
406
+ // Acknowledgements update the existing bubble; they must not restart scrolling.
407
+ if (known) return
408
+ if (this.renderedIds) this.visibleCounts[cid]++
409
+ }
410
+ if (cid !== this.props.cid) {
411
+ const container = this.refBody
412
+ const height = container ? container.scrollHeight : 0
413
+ const top = container ? container.scrollTop : 0
414
+ this.forceUpdate()
415
+ sb.setTimeout(() => {if (container && container === this.refBody) container.scrollTop = top + container.scrollHeight - height})
416
+ return
417
+ }
252
418
  let isBottom = this.isBottom()
419
+ if (newev.type === 'message_sent') this.stopInitialBottom()
253
420
 
254
421
  var byme = false
255
- if (sb.get(newev, 'type') === 'message_sent' && newev.id === sb.get(newev, 'data.message.idempotency_key'))
422
+ if (sb.get(newev, 'type') === 'message_sent' && newev.id === sb.get(newev, 'source_event_id'))
256
423
  byme = true
257
424
  // scroll to bottom when message is sent by me
258
425
  // auto scroll to bottom on new message when we are at the bottom
259
- if (byme || isBottom) this.scrollToBottom()
426
+ if (byme || isBottom) this.scrollToBottom('smooth')
427
+ }
428
+
429
+ historyTarget() {
430
+ const conversations = conversationHistory(this.props.store, this.props.cid)
431
+ const first = conversations.findIndex(convo => this.visibleCounts && this.visibleCounts[convo.id])
432
+ if (first < 0) return
433
+ const convo = conversations[first]
434
+ const count = sb.size(this.props.store.listEvents(convo.id))
435
+ if (this.visibleCounts[convo.id] < count || (!convo.id.startsWith('cslight') && !sb.get(this.props.store.outofmsgM, convo.id))) return convo
436
+ return conversations[first - 1]
437
+ }
438
+
439
+ hasMore() {
440
+ return !!this.historyTarget()
441
+ }
442
+
443
+ async fetchPage(cid) {
444
+ if (cid.startsWith('cslight') || sb.get(this.props.store.outofmsgM, cid)) return {}
445
+ this.eventRequests = this.eventRequests || new Map()
446
+ let request = this.eventRequests.get(cid)
447
+ if (!request) {
448
+ request = this.props.store.fetchMoreEvents(cid, 30)
449
+ this.eventRequests.set(cid, request)
450
+ }
451
+ try {return await request} finally {
452
+ if (this.eventRequests.get(cid) === request) this.eventRequests.delete(cid)
453
+ }
260
454
  }
261
455
 
262
456
  async loadConvo() {
263
- if (!this.props.cid) return
264
- // load msg
265
- this.lastContentOffsetY = 0
266
- this.setState({loading: false})
267
- let events = this.props.store.listEvents(this.props.cid)
268
- if (sb.size(events) === 0) {
269
- this.setState({loading: sb.Date.now()})
270
- sb.setTimeout(() => {
457
+ const cid = this.props.cid
458
+ this.receivedMessageKeys = new Set()
459
+ this.initialBottom = true
460
+ const version = this.loadVersion = (this.loadVersion || 0) + 1
461
+ this.visibleCounts = this.initialVisibleCounts(this.props)
462
+ this.historyBusy = true
463
+ this.evRefs = {}
464
+ this.setState({error: false, historyError: false, historyLoading: false, isShowTyping: false, loading: cid && !sb.size(this.props.store.listEvents(cid)) ? sb.Date.now() : false})
465
+ try {
466
+ if (cid && !sb.size(this.props.store.listEvents(cid))) {
467
+ const result = await this.fetchPage(cid)
468
+ if (version !== this.loadVersion || !this._isMounted) return
469
+ if (result && result.error) this.setState({error: !sb.size(this.props.store.listEvents(cid)), historyError: true})
470
+ }
471
+ } catch (_) {
472
+ if (version === this.loadVersion) this.setState({error: !sb.size(this.props.store.listEvents(cid)), historyError: true})
473
+ } finally {
474
+ if (version === this.loadVersion && this._isMounted) {
475
+ this.historyBusy = false
476
+ this.setState({loading: false})
477
+ if (!this.initialBottom && this.receivedMessageKeys.size && this.isBottom()) this.scrollToBottom('smooth')
478
+ this.queueHistoryCheck()
479
+ // Fetch the previous conversation on opening a draft, even if the greeting fills the viewport.
480
+ if (cid && cid.startsWith('cslightwidget_') && Object.keys(this.visibleCounts).length === 1 && this.hasMore()) this.loadMore()
481
+ }
482
+ }
483
+ }
484
+
485
+ async loadMore() {
486
+ if (this.historyBusy || !this._isMounted) return
487
+ const target = this.historyTarget()
488
+ if (!target) return
489
+ const version = this.loadVersion
490
+ this.historyBusy = true
491
+ const container = this.refBody
492
+ const visible = this.visibleCounts[target.id] || 0
493
+ this.setState({historyLoading: true})
494
+ try {
495
+ if (visible >= sb.size(this.props.store.listEvents(target.id))) {
496
+ const result = await this.fetchPage(target.id)
497
+ if (result && result.error) throw result.error
498
+ }
499
+ if (version !== this.loadVersion || !this._isMounted) return
500
+ this.visibleCounts[target.id] = visible + 30
501
+ this.setState({historyError: false})
502
+ } catch (_) {
503
+ if (version === this.loadVersion) this.setState({historyError: true})
504
+ } finally {
505
+ if (version === this.loadVersion && this._isMounted) {
506
+ // The reader may have scrolled or sent a message while the request was pending.
507
+ const height = container ? container.scrollHeight : 0
508
+ const top = container ? container.scrollTop : 0
509
+ this.setState({historyLoading: false})
271
510
  this.forceUpdate()
272
- }, 800)
273
- let cid = this.props.cid
274
-
275
- let {error} = await this.props.store.fetchMoreEvents(cid)
276
- this.setState({loading: false})
277
- if (cid !== this.props.cid) return // out dated
278
- if (error) {
279
- this.setState({error: true})
280
- return
511
+ await new Promise(resolve => sb.setTimeout(resolve, 0))
512
+ if (version === this.loadVersion && this._isMounted) {
513
+ if (this.initialBottom) this.restoreInitialBottom()
514
+ else if (container && container === this.refBody) {
515
+ container.scrollTop = top + container.scrollHeight - height
516
+ this.lastScrollTop = container.scrollTop
517
+ }
518
+ this.historyBusy = false
519
+ this.queueHistoryCheck()
520
+ }
281
521
  }
282
522
  }
283
- this.props.store.fetchMoreEvents(this.props.cid)
284
- this.setState({error: false})
285
- this.scrollToBottom()
286
- // sb.setTimeout(() => this.setState({loading: false}), 50)
287
523
  }
288
524
 
289
525
  isBottom() {
@@ -292,15 +528,21 @@ export default class Events extends Com {
292
528
  return $container.scrollHeight < $container.scrollTop + $container.offsetHeight + 50
293
529
  }
294
530
 
295
- scrollToBottom() {
296
- sb.setTimeout(() => {
531
+ scrollToBottom(behavior = 'auto') {
532
+ if (this.scrollTimer) sb.clearTimeout(this.scrollTimer)
533
+ const cid = this.props.cid
534
+ this.scrollTimer = sb.setTimeout(() => {
535
+ this.scrollTimer = null
536
+ if (!this._isMounted || this.props.cid !== cid) return
297
537
  const $div = this.refBody
298
- if ($div) $div.scrollTop = $div.scrollHeight
538
+ if (!$div) return
539
+ const reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches
540
+ if (behavior === 'smooth' && !reducedMotion && $div.scrollTo) {
541
+ $div.scrollTo({top: $div.scrollHeight, behavior: 'smooth'})
542
+ } else {
543
+ $div.scrollTop = $div.scrollHeight
544
+ }
299
545
  })
300
- sb.setTimeout(() => {
301
- const $div = this.refBody
302
- if ($div) $div.scrollTop = $div.scrollHeight
303
- }, 100)
304
546
 
305
547
  if (this.props.isShow) {
306
548
  if (this.props.store.browserVisibility === 'visible' && document.hasFocus()) {
@@ -312,76 +554,6 @@ export default class Events extends Com {
312
554
 
313
555
  // [{ev: {}, mode: 'time'}]
314
556
  //_mode = time
315
- //_mode = lead
316
- function processEvents(convo, events) {
317
- events = events.filter((event) => visibleEvent(event.ev))
318
- if (events.length < 1) return
319
-
320
- // show time for the first msg sent
321
- // insert time
322
- var lastAgentEvent
323
- for (var i = 1; i < events.length; i++) {
324
- let curEvent = events[i]
325
- let lastEvent = events[i - 1]
326
- if (sb.get(curEvent, 'ev.by.type') != 'user' && !curEvent.is_typing) {
327
- lastAgentEvent = curEvent
328
- }
329
-
330
- if (visibleEvent(curEvent.ev)) {
331
- let elapsedSec = sb.unixSec(curEvent.ev.created) - sb.unixSec(lastEvent.ev.created)
332
- if (elapsedSec > 300) curEvent.show_time = true // 5 mins
333
- }
334
- }
335
-
336
- if (lastAgentEvent) lastAgentEvent.show_time = true
337
-
338
- for (var i = 0; i < events.length; i++) {
339
- let curEvent = events[i]
340
- if (sb.get(curEvent, 'ev.type') === 'message_sent') {
341
- curEvent.show_time = true
342
- break
343
- }
344
- }
345
-
346
- let lastMsgSentIdx = -1
347
- // insert lead
348
- for (var i = 0; i < events.length; i++) {
349
- let curEvent = events[i]
350
- let prevEvent = events[i - 1]
351
- let nextEvent = events[i + 1]
352
-
353
- if (curEvent.ev.type === 'message_sent') {
354
- // ignore fake typing message_sent event at last
355
- lastMsgSentIdx = i
356
- curEvent.mode = 'lead'
357
- if (
358
- sb.get(prevEvent, 'ev.type') === 'message_sent' &&
359
- sb.get(prevEvent, 'ev.by.id') === sb.get(curEvent, 'ev.by.id')
360
- ) {
361
- curEvent.mode = 'follow'
362
- }
363
- if (
364
- sb.get(nextEvent, 'ev.type') !== 'message_sent' ||
365
- sb.get(nextEvent, 'ev.by.id') !== sb.get(curEvent, 'ev.by.id')
366
- ) {
367
- curEvent.is_end = true
368
- }
369
- continue
370
- }
371
- }
372
- if (lastMsgSentIdx > -1) {
373
- events[lastMsgSentIdx].is_last_message_send = true
374
- }
375
- }
376
-
377
- function visibleEvent(ev) {
378
- let type = sb.get(ev, 'type')
379
- if (type === 'message_sent') return true
380
- if (type === 'conversation_state_updated') {
381
- return sb.get(ev, 'data.conversation.state') == 'ended'
382
- }
383
- }
384
-
385
557
  function isSummited(form) {
386
558
  let fields = sb.get(form, 'fields') || []
387
559
  return sb.find(fields, (field) => !!field.answered)
@@ -191,7 +191,7 @@ export default class FormAttachment extends Com {
191
191
  }}
192
192
  type="checkbox"
193
193
  id={'checkbox.' + this.props.ev.id + '.' + field.name}
194
- onChange={e => this.onInputChange(index, ev.target.checked)}
194
+ onChange={e => this.onInputChange(index, e.target.checked)}
195
195
  checked={field.value}
196
196
  />
197
197
  <label class="checkbox--label" for={'checkbox.' + this.props.ev.id + '.' + field.name}>
@@ -223,13 +223,13 @@ Vui lòng hỏi chúng tôi bất cứ điều gì hoặc chia sẻ phản hồi
223
223
  $unreadcount = <span class='widget-header--total-unread-convo'>{unreadcount}</span>
224
224
  }
225
225
  return (
226
- <div
226
+ <button type='button' aria-label={this.props.t('menu_back')}
227
227
  class={`widget-header--wrap-button-back ${unreadcount ? 'widget-header--has-convo-unred' : ''}`}
228
228
  onClick={(e) => this.onBackClick(e)}
229
229
  >
230
230
  <div class='widget-header--button-back' data-tooltip={this.props.t('menu_back')}></div>
231
231
  {$unreadcount}
232
- </div>
232
+ </button>
233
233
  )
234
234
  }
235
235
 
@@ -238,14 +238,14 @@ Vui lòng hỏi chúng tôi bất cứ điều gì hoặc chia sẻ phản hồi
238
238
  if (accSetting.hide_close_button) return null
239
239
  if (this.props.store.isEmbededChatMode()) return
240
240
  return (
241
- <div
241
+ <button type='button' aria-label={this.props.t('close')}
242
242
  class='widget-header--button-close'
243
243
  data-tooltip={this.props.t('close')}
244
244
  data-tooltip-position='bottom'
245
245
  onClick={(e) => this.onCloseClick(e)}
246
246
  >
247
247
  <span class='widget-header--button-close-icon'></span>
248
- </div>
248
+ </button>
249
249
  )
250
250
  }
251
251
 
@@ -29,7 +29,7 @@ export default class InfoMessage extends Com {
29
29
  if (ev.type === 'conversation_state_updated' && sb.get(ev, 'data.conversation.state') === 'ended') {
30
30
  return (
31
31
  <div class='convo-event'>
32
- <div class='info-message'>{this.props.t('conversation_ended')}</div>
32
+ <div class='info-message info-message--ended'>{this.props.t('conversation_ended')}</div>
33
33
  </div>
34
34
  )
35
35
  }