@subiz/widget-v4 4.1.210 → 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 (124) 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/{1a0d80f6e72.132.js → 1a0ddc53cbb.132.js} +1 -1
  46. package/dist/1a0ddc53cbb.278.js +1 -0
  47. package/dist/{1a0d80f6e72.339.js → 1a0ddc53cbb.339.js} +1 -1
  48. package/dist/1a0ddc53cbb.app.js +1 -0
  49. package/dist/{1a0d80f6e72.chat.html → 1a0ddc53cbb.chat.html} +1 -1
  50. package/dist/1a0ddc53cbb.chat.js +1 -0
  51. package/dist/{1a0d80f6e72.form.html → 1a0ddc53cbb.form.html} +1 -1
  52. package/dist/1a0ddc53cbb.form.js +1 -0
  53. package/dist/{1a0d80f6e72.index.html → 1a0ddc53cbb.index.html} +1 -1
  54. package/dist/{1a0d80f6e72.library.html → 1a0ddc53cbb.library.html} +1 -1
  55. package/dist/1a0ddc53cbb.library.js +1 -0
  56. package/dist/{1a0d80f6e72.preview.html → 1a0ddc53cbb.preview.html} +1 -1
  57. package/dist/1a0ddc53cbb.preview.js +1 -0
  58. package/dist/1a0ddc53cbb.webview.js +1 -0
  59. package/dist/screenshoot.js +1492 -616
  60. package/package.json +10 -3
  61. package/src/chatbox/Buttons.js +5 -4
  62. package/src/chatbox/ChatButton.js +120 -39
  63. package/src/chatbox/Event.js +38 -5
  64. package/src/chatbox/Events.js +306 -134
  65. package/src/chatbox/FormAttachment.js +1 -1
  66. package/src/chatbox/Header.js +4 -4
  67. package/src/chatbox/InfoMessage.js +1 -1
  68. package/src/chatbox/MessageInput.js +15 -10
  69. package/src/chatbox/OrderAttachments.js +13 -13
  70. package/src/chatbox/QuoteMessage.js +3 -2
  71. package/src/chatbox/TextMessage.js +21 -20
  72. package/src/chatbox/Widget.js +150 -150
  73. package/src/chatbox/block.js +29 -27
  74. package/src/chatbox/conversation-history.js +37 -0
  75. package/src/chatbox/event-layout.js +72 -0
  76. package/src/chatbox/greeting.js +13 -0
  77. package/src/common.js +15 -9
  78. package/src/lib.js +22 -35
  79. package/src/plugin.js +11 -13
  80. package/src/popup/popup.js +1 -1
  81. package/src/store/api.js +0 -2
  82. package/src/store/api_fake.js +1 -1
  83. package/src/store/attachments.js +21 -0
  84. package/src/store/chain.js +25 -12
  85. package/src/store/kv.js +10 -19
  86. package/src/store/lsdb.js +1 -1
  87. package/src/store/store.js +313 -130
  88. package/src/util/common.js +48 -43
  89. package/src/util/event.js +7 -0
  90. package/src/util/pubsub.js +27 -8
  91. package/src/util/url.js +14 -0
  92. package/test.cjs +59 -0
  93. package/assets/less/contact.less +0 -126
  94. package/compiled_src/util/common.test.js +0 -414
  95. package/compiled_src/util/event.test.js +0 -38
  96. package/compiled_src/util/extractor.test.js +0 -299
  97. package/compiled_src/util/extractor.test2.js +0 -61
  98. package/compiled_src/util/gate.test.js +0 -18
  99. package/compiled_src/util/pubsub.test.js +0 -35
  100. package/dist/1a0d80f6e72.127.js +0 -1
  101. package/dist/1a0d80f6e72.278.js +0 -1
  102. package/dist/1a0d80f6e72.74.js +0 -1
  103. package/dist/1a0d80f6e72.997.js +0 -1
  104. package/dist/1a0d80f6e72.app.js +0 -1
  105. package/dist/1a0d80f6e72.chat.js +0 -1
  106. package/dist/1a0d80f6e72.form.js +0 -1
  107. package/dist/1a0d80f6e72.library.js +0 -1
  108. package/dist/1a0d80f6e72.preview.js +0 -1
  109. package/dist/1a0d80f6e72.webview.js +0 -1
  110. package/dist/74.js +0 -376
  111. package/dist/997.js +0 -37
  112. package/dist/HASH +0 -1
  113. package/src/contact/contact.js +0 -400
  114. package/src/contact/css.js +0 -1
  115. package/src/util/common.test.js +0 -301
  116. package/src/util/event.test.js +0 -31
  117. package/src/util/extractor.test.js +0 -240
  118. package/src/util/extractor.test2.js +0 -56
  119. package/src/util/gate.test.js +0 -20
  120. package/src/util/pubsub.test.js +0 -37
  121. /package/dist/{1a0d80f6e72.185.js → 1a0ddc53cbb.185.js} +0 -0
  122. /package/dist/{1a0d80f6e72.27.js → 1a0ddc53cbb.27.js} +0 -0
  123. /package/dist/{1a0d80f6e72.459.js → 1a0ddc53cbb.459.js} +0 -0
  124. /package/dist/{1a0d80f6e72.490.js → 1a0ddc53cbb.490.js} +0 -0
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@subiz/widget-v4",
3
- "version": "4.1.210",
3
+ "version": "4.1.211",
4
4
  "main": "./compiled_src/lib.js",
5
5
  "scripts": {
6
6
  "dev": "npx webpack serve --config webpack.dev.js",
7
- "build": "rm -rf dist && npx webpack --config webpack.screenshoot.js && npx webpack --config webpack.prod.js",
7
+ "build": "npm run lib && rm -rf dist && webpack --config webpack.screenshoot.js && webpack --config webpack.prod.js",
8
8
  "report": "npx webpack --config webpack.report.js",
9
- "lib": "rm -rf compiled_src && npx babel --out-dir=compiled_src src"
9
+ "lib": "babel src --out-dir compiled_src --ignore \"**/*.test.js,**/*.test2.js\"",
10
+ "test": "node test.cjs",
11
+ "lint": "eslint src",
12
+ "check": "npm run lint && npm test",
13
+ "prepack": "npm run check && npm run lib"
10
14
  },
11
15
  "dependencies": {
12
16
  "@subiz/ajax": "^3.0.22",
@@ -28,7 +32,10 @@
28
32
  "@babel/preset-env": "^7.29.7",
29
33
  "babel-loader": "^10.1.1",
30
34
  "babel-plugin-syntax-jsx": "^6.18.0",
35
+ "eslint": "^10.11.0",
36
+ "globals": "^17.12.0",
31
37
  "html-webpack-plugin": "^5.6.8",
38
+ "jsdom": "^30.1.1",
32
39
  "puppeteer": "^25.12.0",
33
40
  "tape": "^5.10.2",
34
41
  "webpack": "^5.111.1",
@@ -1,12 +1,13 @@
1
1
  import sb from '../util/index.js'
2
2
  import {h} from 'preact'
3
+ import {safeUrl} from '../util/url.js'
3
4
 
4
5
  export default function Buttons(props) {
5
6
  if (sb.size(props.buttons) === 0) return null
6
7
  var $buttons = sb.map(props.buttons, (button, index) => {
7
8
  if (button.type === 'call_button') {
8
9
  return (
9
- <a class='url-button-attachment' href={'tel:' + button.phone_number} key={index} title={button.title}>
10
+ <a class='url-button-attachment' href={safeUrl('tel:' + button.phone_number)} key={index} title={button.title}>
10
11
  {props.t.i18n(button.i18n_title) || props.t.i18n(button.old_i18n_title) || button.title || `${props.t('call')} ${button.phone_number}`}
11
12
  </a>
12
13
  )
@@ -14,7 +15,7 @@ export default function Buttons(props) {
14
15
 
15
16
  if (button.type === 'url_button') {
16
17
  return (
17
- <a class='url-button-attachment' href={button.url} target='_blank' key={index} title={button.title}>
18
+ <a class='url-button-attachment' href={safeUrl(button.url) || undefined} target='_blank' rel='noopener noreferrer' key={index} title={button.title}>
18
19
  {props.t.i18n(button.i18n_title) || props.t.i18n(button.old_i18n_title) || button.title || button.url}
19
20
  </a>
20
21
  )
@@ -22,9 +23,9 @@ export default function Buttons(props) {
22
23
 
23
24
  if (button.type === 'postback_button') {
24
25
  return (
25
- <a class='url-button-attachment' href='javascript:;' onClick={(_) => props.onSelect(button)}>
26
+ <button type='button' class='url-button-attachment' key={index} onClick={(_) => props.onSelect(button)}>
26
27
  {props.t.i18n(button.i18n_title) || button.title}
27
- </a>
28
+ </button>
28
29
  )
29
30
  }
30
31
 
@@ -378,30 +378,7 @@ export default class ChatButton extends Com {
378
378
  )
379
379
  }
380
380
 
381
- async prepareButtonImage() {
382
- let accountSetting = this.props.plugin.chatbox
383
- accountSetting = this.fixMissingSetting(accountSetting)
384
- let imgsrc =
385
- sb.get(accountSetting, 'desktop_button.background_image_url') ||
386
- sb.get(accountSetting, 'desktop_button.background_mask_url') ||
387
- defaultBubble
388
-
389
- if (this.props.store.isMobile()) {
390
- imgsrc =
391
- sb.get(accountSetting, 'mobile_button.background_image_url') ||
392
- sb.get(accountSetting, 'mobile_button.background_mask_url') ||
393
- imgsrc
394
- }
395
-
396
- imgsrc = this.props.store.replaceFileUrl(imgsrc)
397
- await sb.preloadImg(imgsrc)
398
- this.setState({ready: true})
399
- this.readyGate.open()
400
- }
401
-
402
- renderButton() {
403
- if (typeof sbz_info != 'undefined') sbz_info.render_chatbox_button = sb.Date.now()
404
-
381
+ getButtonAppearance() {
405
382
  let accountSetting = this.props.plugin.chatbox
406
383
  accountSetting = this.fixMissingSetting(accountSetting)
407
384
  let imgsrc =
@@ -433,24 +410,88 @@ export default class ChatButton extends Com {
433
410
  }
434
411
  }
435
412
 
413
+ const imageUrl = (url) => addVersionParam(this.props.store.replaceFileUrl(url))
414
+ imgsrc = imageUrl(imgsrc)
415
+ imgurl = imageUrl(imgurl)
416
+ maskurl = imageUrl(maskurl)
417
+ const defaultMask = !imgurl && !maskurl ? DEF_BG_URL : ''
418
+ const urls = [
419
+ ...new Set([imgsrc, imgurl && encodeURI(imgurl), maskurl && encodeURI(maskurl), defaultMask].filter(Boolean)),
420
+ ]
421
+ return {
422
+ imgsrc,
423
+ imgurl,
424
+ maskurl,
425
+ background,
426
+ position,
427
+ imgwidth,
428
+ imgheight,
429
+ urls,
430
+ maskAsset: maskurl ? encodeURI(maskurl) : defaultMask,
431
+ key: JSON.stringify([urls, maskurl, defaultMask]),
432
+ }
433
+ }
434
+
435
+ componentDidUpdate() {
436
+ if (this.getButtonAppearance().key !== this._buttonImageKey) this.prepareButtonImage()
437
+ }
438
+
439
+ async prepareButtonImage() {
440
+ const appearance = this.getButtonAppearance()
441
+ const version = (this._buttonImageVersion = (this._buttonImageVersion || 0) + 1)
442
+ this._buttonImageKey = appearance.key
443
+ this.readyGate.close()
444
+ const [loaded, mask] = await Promise.all([
445
+ Promise.all(appearance.urls.filter((url) => url !== appearance.maskAsset).map((url) => preloadButtonImage(url))),
446
+ appearance.maskAsset ? prepareButtonMask(appearance.maskAsset) : Promise.resolve(null),
447
+ ])
448
+ const stale = () => !this._isMounted || this._buttonImageVersion !== version
449
+ if (stale()) {
450
+ if (mask) URL.revokeObjectURL(mask)
451
+ return
452
+ }
453
+ const fallback = loaded.some((success) => !success) || (!!appearance.maskAsset && !mask)
454
+ const ready = !fallback || (await preloadButtonImage(defaultBubble))
455
+ if (stale()) {
456
+ if (mask) URL.revokeObjectURL(mask)
457
+ return
458
+ }
459
+ if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl)
460
+ this._buttonMaskUrl = mask
461
+ this.setState({ready, readyImageKey: appearance.key, fallbackButtonImage: fallback, buttonMaskUrl: mask})
462
+ this.readyGate.open()
463
+ }
464
+
465
+ componentWillUnmount() {
466
+ if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl)
467
+ if (this._animateTimeout) clearTimeout(this._animateTimeout)
468
+ }
469
+
470
+ renderButton() {
471
+ if (typeof sbz_info != 'undefined') sbz_info.render_chatbox_button = sb.Date.now()
472
+ const appearance = this.getButtonAppearance()
473
+ let {imgsrc, imgurl, maskurl, background, position, imgwidth, imgheight} = appearance
474
+ // Never attach a remote CSS mask while the local copy is being prepared.
475
+ if (appearance.maskAsset) {
476
+ maskurl = this.state.readyImageKey === appearance.key ? this.state.buttonMaskUrl : ''
477
+ if (imgsrc === appearance.maskAsset) imgsrc = maskurl || ''
478
+ }
479
+ if (this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage) {
480
+ imgsrc = imgurl = defaultBubble
481
+ maskurl = ''
482
+ }
483
+
436
484
  let whitebgcls = 'chat-button--white-bg'
437
485
  if (!imgurl && maskurl) whitebgcls += ' chat-button--white-bg--mask-only'
438
486
  let imgcontainerstyle = ''
439
487
 
440
- imgurl = this.props.store.replaceFileUrl(imgurl)
441
- imgsrc = this.props.store.replaceFileUrl(imgsrc)
442
- maskurl = this.props.store.replaceFileUrl(maskurl)
443
- imgurl = addVersionParam(imgurl)
444
- imgsrc = addVersionParam(imgsrc)
445
- maskurl = addVersionParam(maskurl)
446
-
447
488
  if (!imgurl && !maskurl) {
448
489
  whitebgcls += ' chat-button--white-bg--mask-only'
449
490
  imgcontainerstyle = `
450
- -webkit-mask-image: url("${encodeURI(DEF_BG_URL)}") !important;
451
- -o-mask-image: url("${encodeURI(DEF_BG_URL)}") !important;
452
- -moz-mask-image: url("${encodeURI(DEF_BG_URL)}") !important;
453
- mask-image: url("${encodeURI(DEF_BG_URL)}") !important;
491
+ -webkit-mask-image: none !important;
492
+ -o-mask-image: none !important;
493
+ -moz-mask-image: none !important;
494
+ mask-image: none !important;
454
495
  height: ${imgheight}px !important;
455
496
  width: ${imgwidth}px !important;
456
497
  background: ${background} !important;
@@ -509,16 +550,22 @@ export default class ChatButton extends Com {
509
550
 
510
551
  `
511
552
 
512
- if (!this.state.ready) cls += ' hidden'
553
+ if (!this.state.ready || this.state.readyImageKey !== appearance.key) cls += ' hidden'
513
554
 
514
555
  return (
515
- <div class={cls} onClick={(e) => this.onClick(e)}>
556
+ <button
557
+ type='button'
558
+ class={cls}
559
+ aria-label={this.props.t('chat')}
560
+ aria-expanded={!this.props.isShow}
561
+ onClick={(e) => this.onClick(e)}
562
+ >
516
563
  <div class={whitebgcls} style={whiteBgStyle}></div>
517
564
  <div class='chat-button--img-container' style={imgcontainerstyle}>
518
565
  <img width='70' height='70' alt='chat button' class='chat-button--img' src={imgsrc} />
519
566
  </div>
520
567
  {this.renderUnread()}
521
- </div>
568
+ </button>
522
569
  )
523
570
  }
524
571
 
@@ -551,7 +598,7 @@ export default class ChatButton extends Com {
551
598
  }
552
599
  return defmsg
553
600
  }
554
- return <Block block={block} />
601
+ return <Block block={block} store={this.props.store} t={this.props.t} />
555
602
  }
556
603
 
557
604
  let message = sb.get(ev, 'data.message.text', defmsg)
@@ -600,3 +647,37 @@ function addVersionParam(url, version = 8) {
600
647
  return hash ? newUrl + '#' + hash : newUrl
601
648
  }
602
649
  }
650
+
651
+ // Use the exact rendered URL, including its version, for both images and masks.
652
+ function preloadButtonImage(src, {crossOrigin = false} = {}) {
653
+ return new Promise((resolve) => {
654
+ const image = new Image()
655
+ if (crossOrigin) image.crossOrigin = 'anonymous'
656
+ image.onerror = () => resolve(false)
657
+ image.onload = async () => {
658
+ try {
659
+ if (image.decode) await image.decode()
660
+ resolve(true)
661
+ } catch (_) {
662
+ resolve(false)
663
+ }
664
+ }
665
+ image.src = src
666
+ })
667
+ }
668
+
669
+ // CSS masks use a separate CORS image request. Pin the loaded bytes locally so
670
+ // revealing the white backing never races another network request by CSS.
671
+ async function prepareButtonMask(src) {
672
+ let url
673
+ try {
674
+ const response = await fetch(src)
675
+ if (!response.ok) return null
676
+ url = URL.createObjectURL(await response.blob())
677
+ if (await preloadButtonImage(url)) return url
678
+ } catch (_) {
679
+ // Use the bundled fallback if the custom mask cannot be loaded.
680
+ }
681
+ if (url) URL.revokeObjectURL(url)
682
+ return null
683
+ }
@@ -28,11 +28,31 @@ export default class Event extends Com {
28
28
  this.props.store.onConvoEvent(this, (ev) => this.onEvent(ev))
29
29
  this.refForm = createRef()
30
30
  this.state = {pongs: sb.get(this.props.event, 'ev.data.message.pongs') || []}
31
+ const ev = this.props.event.ev
32
+ const age = (this.props.store.now ? this.props.store.now() : sb.Date.now()) - ev.created
33
+ // Capture this once: acknowledgement keeps the same component and animation.
34
+ this.animateSend = !props.isHistory && ev.type === 'message_sent' &&
35
+ sb.get(ev, 'by.type') === 'user' && ev.id === sb.get(ev, 'source_event_id') &&
36
+ age >= 0 && age < 2000
31
37
  this.textMessageRef = createRef()
32
38
  this.fileAttachmentRef = createRef()
33
39
  this.imageAttachmentRef = createRef()
34
40
  }
35
41
 
42
+ componentDidMount() {
43
+ if (!this.animateSend) return
44
+ // Paint the starting position before transitioning to the resting position.
45
+ this.sendAnimationFrame = window.requestAnimationFrame(() => {
46
+ this.sendAnimationFrame = window.requestAnimationFrame(() => {
47
+ if (this._isMounted) this.setState({sendEntered: true})
48
+ })
49
+ })
50
+ }
51
+
52
+ componentWillUnmount() {
53
+ if (this.sendAnimationFrame) window.cancelAnimationFrame(this.sendAnimationFrame)
54
+ }
55
+
36
56
  focusSourceEventFromQuote = (ev) => {
37
57
  let $textMessage = this.textMessageRef.current
38
58
  if ($textMessage && typeof $textMessage.focusSourceEventFromQuote === 'function') {
@@ -90,6 +110,7 @@ export default class Event extends Com {
90
110
  }
91
111
 
92
112
  chooseQuickReply(reply) {
113
+ if (this.props.isHistory) return
93
114
  let ev = sb.get(this.props.event, 'ev', {})
94
115
  let message = {text: reply.title, format: 'plaintext'}
95
116
  if (reply.payload) {
@@ -107,6 +128,7 @@ export default class Event extends Com {
107
128
  }
108
129
 
109
130
  choosePostbackBtn(button) {
131
+ if (this.props.isHistory) return
110
132
  let ev = sb.get(this.props.event, 'ev', {})
111
133
  let message = {text: button.title, format: 'plaintext'}
112
134
  if (button.payload) {
@@ -252,6 +274,14 @@ export default class Event extends Com {
252
274
 
253
275
  renderMessageSent() {
254
276
  let ev = this.props.event.ev
277
+ if (sb.get(ev, 'data.message.deleted')) {
278
+ let byType = sb.get(ev, 'by.type') === 'user' ? 'user' : 'agent'
279
+ return (
280
+ <div class={'convo-event ' + byType + '-message'} data-ev-id={ev.id}>
281
+ <TextMessage ev={ev} t={this.props.t} store={this.props.store} />
282
+ </div>
283
+ )
284
+ }
255
285
  let attachments = sb.get(ev, 'data.message.attachments')
256
286
  let hasForm = sb.find(attachments, (att) => att.type === 'form')
257
287
  if (sb.isEmptyMessage(sb.get(ev, 'data.message'))) return null
@@ -312,7 +342,7 @@ export default class Event extends Com {
312
342
  ev={ev}
313
343
  />
314
344
  <OrderAttachments t={this.props.t} store={this.props.store} ev={ev} />
315
- <QuickReplies store={this.props.store} ev={ev} onSelect={(e) => this.chooseQuickReply(e)} />
345
+ {!this.props.isHistory && <QuickReplies store={this.props.store} ev={ev} onSelect={(e) => this.chooseQuickReply(e)} />}
316
346
  {this.renderReactions()}
317
347
  {this.renderAIDevNote()}
318
348
  </div>
@@ -321,14 +351,14 @@ export default class Event extends Com {
321
351
  )
322
352
  }
323
353
 
324
- if (sb.get(ev, 'data.message.idempotency_key') === ev.id && this.props.store.now() - ev.created < 900) {
354
+ if (sb.get(ev, 'source_event_id') === ev.id && this.props.store.now() - ev.created < 900) {
325
355
  sb.setTimeout(() => this.setState({refresh: this.state.refresh}), 1000)
326
356
  }
327
357
 
328
358
  // only show sending while the message is send too long (>300ms) but haven't returned from
329
359
  // the server
330
360
  let $dot = <div class='sending_dot'></div>
331
- if (this.props.store.now() - ev.created < 900 || sb.get(ev, 'data.message.idempotency_key') !== ev.id) {
361
+ if (this.props.store.now() - ev.created < 900 || sb.get(ev, 'source_event_id') !== ev.id) {
332
362
  $dot = <div class='sending_dot sending_dot__hide'></div>
333
363
  }
334
364
 
@@ -352,11 +382,12 @@ export default class Event extends Com {
352
382
  return (
353
383
  <div class='convo-event user-message' data-ev-id={ev.id}>
354
384
  {$sender}
355
- <div class='message-user__container'>
385
+ <div class={'message-user__container' + (this.animateSend && !this.props.isHistory ? ' message-user__container--send' + (this.state.sendEntered ? '' : ' message-user__container--enter') : '')}>
356
386
  {$dot}
357
387
  <TextMessage
358
388
  ref={this.textMessageRef}
359
389
  store={this.props.store}
390
+ t={this.props.t}
360
391
  ev={ev}
361
392
  refresh={this.state.refresh}
362
393
  onLink={this.props.onLink}
@@ -376,6 +407,7 @@ export default class Event extends Com {
376
407
  }
377
408
 
378
409
  async onSendReaction(action) {
410
+ if (this.props.isHistory) return
379
411
  if (this._reacting) return
380
412
  let ev = this.props.event.ev
381
413
  let cid = sb.get(ev, 'data.message.conversation_id')
@@ -423,6 +455,7 @@ export default class Event extends Com {
423
455
  }
424
456
 
425
457
  renderReactions() {
458
+ if (this.props.isHistory) return null
426
459
  if (this.props.store.isAIDevMode()) return null
427
460
  let cid = sb.get(this.props.event.ev, 'data.message.conversation_id', '')
428
461
  if (cid.startsWith('cslight')) return null
@@ -515,7 +548,7 @@ export default class Event extends Com {
515
548
  let byid = sb.get(ev, 'by.display_id', sb.get(ev, 'by.id')) || ''
516
549
  if (bytype === 'user') return sb.getUserDisplayName(this.props.store.matchUser())
517
550
  let acc = this.props.store.matchAccount()
518
- if (bytype === 'agent') return sb.getAgentDisplayName(acc, this.props.store.matchAgent(byid))
551
+ if (bytype === 'agent' || sb.get(ev, 'by.display_type') === 'agent') return sb.getAgentDisplayName(acc, this.props.store.matchAgent(byid))
519
552
  if (byid.startsWith('at')) return sb.getAgentDisplayName(acc, this.props.store.matchAgent(byid))
520
553
  if (bytype === 'bot') {
521
554
  let bot = this.props.store.matchBot()[byid]