@zenify-omni/chat-ui-react-js 0.1.0

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 (403) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/assets/asset-url.js +6 -0
  4. package/assets/icons/chatter_agent.js +27 -0
  5. package/assets/icons/chatter_chatbot.js +43 -0
  6. package/assets/icons/chatter_contact.js +36 -0
  7. package/assets/icons/email_icon.png +0 -0
  8. package/assets/icons/emoji/angry.svg +56 -0
  9. package/assets/icons/emoji/haha.svg +20 -0
  10. package/assets/icons/emoji/like.svg +10 -0
  11. package/assets/icons/emoji/like_out_line.png +0 -0
  12. package/assets/icons/emoji/love.svg +10 -0
  13. package/assets/icons/emoji/sad.svg +46 -0
  14. package/assets/icons/emoji/wow.svg +51 -0
  15. package/assets/icons/instagram.js +29 -0
  16. package/assets/icons/not_found.png +0 -0
  17. package/assets/icons/not_found_template.svg +9 -0
  18. package/assets/icons/phone_icon.png +0 -0
  19. package/assets/icons/shopee.js +17 -0
  20. package/assets/icons/sms_icon.png +0 -0
  21. package/assets/icons/tiktok.js +17 -0
  22. package/assets/icons/whatsapp.js +28 -0
  23. package/assets/icons/zalo.js +17 -0
  24. package/assets/icons/zalo_icon.png +0 -0
  25. package/assets/icons/zns_icon.png +0 -0
  26. package/assets/images/emoji.webp +0 -0
  27. package/assets/images/error.png +0 -0
  28. package/assets/images/menu.png +0 -0
  29. package/assets/images/money.png +0 -0
  30. package/assets/images/success.png +0 -0
  31. package/assets/public-urls.js +78 -0
  32. package/components/campaign-detail-route/select/group.js +38 -0
  33. package/components/campaign-detail-route/select/multi-groups.js +43 -0
  34. package/components/chat-route/chat-box/ads-context-card.js +143 -0
  35. package/components/chat-route/chat-box/chat-drop-file.js +18 -0
  36. package/components/chat-route/chat-box/chat-feedback/chat-insight.js +214 -0
  37. package/components/chat-route/chat-box/chat-feedback/index.js +127 -0
  38. package/components/chat-route/chat-box/chat-header-actions-inline.js +1055 -0
  39. package/components/chat-route/chat-box/chat-header.js +465 -0
  40. package/components/chat-route/chat-box/chat-input.js +1164 -0
  41. package/components/chat-route/chat-box/chat-message-action.js +399 -0
  42. package/components/chat-route/chat-box/chat-message-list.js +995 -0
  43. package/components/chat-route/chat-box/chat-message.js +2099 -0
  44. package/components/chat-route/chat-box/chat-reply-preview.js +317 -0
  45. package/components/chat-route/chat-box/common-groups-modal.js +430 -0
  46. package/components/chat-route/chat-box/constants.js +29 -0
  47. package/components/chat-route/chat-box/contact-card/index.js +87 -0
  48. package/components/chat-route/chat-box/edit-customer-alias-dialog.js +197 -0
  49. package/components/chat-route/chat-box/emoji-picker-combined.js +91 -0
  50. package/components/chat-route/chat-box/emoji-sprite.js +45 -0
  51. package/components/chat-route/chat-box/emojiMap.js +92 -0
  52. package/components/chat-route/chat-box/facebook-reconnect-notice.js +49 -0
  53. package/components/chat-route/chat-box/forward-message-modal.js +659 -0
  54. package/components/chat-route/chat-box/index.js +165 -0
  55. package/components/chat-route/chat-box/message-time.js +44 -0
  56. package/components/chat-route/chat-box/message-viewers.js +114 -0
  57. package/components/chat-route/chat-box/pasted-file-preview.js +138 -0
  58. package/components/chat-route/chat-box/pinned-messages.js +255 -0
  59. package/components/chat-route/chat-box/react-message/react-preview.js +83 -0
  60. package/components/chat-route/chat-box/react-message/reaction-button.js +200 -0
  61. package/components/chat-route/chat-box/right-side/chat-history.js +142 -0
  62. package/components/chat-route/chat-box/right-side/chat-tag-selector.js +232 -0
  63. package/components/chat-route/chat-box/right-side/create-personal-tag-modal.js +458 -0
  64. package/components/chat-route/chat-box/right-side/customer/customer-info-item.js +60 -0
  65. package/components/chat-route/chat-box/right-side/in-room-dialog.js +225 -0
  66. package/components/chat-route/chat-box/right-side/zalo-group/add-friend-dialog.js +154 -0
  67. package/components/chat-route/chat-box/right-side/zalo-group/send-message-dialog.js +165 -0
  68. package/components/chat-route/chat-box/send-contact-card-modal.js +102 -0
  69. package/components/chat-route/chat-box/send-contact-card.js +80 -0
  70. package/components/chat-route/chat-box/text-content.js +400 -0
  71. package/components/chat-route/chat-box/use-facebook-page-invalid.js +22 -0
  72. package/components/chat-route/chat-box/zalo-emoji-decoration.js +98 -0
  73. package/components/chat-route/chat-box/zalo-emoji-picker.js +74 -0
  74. package/components/chat-route/chat-box/zalo-personal-editor.js +590 -0
  75. package/components/chat-route/chat-box/zalo-reminder/create-zalo-reminder.js +868 -0
  76. package/components/chat-route/chat-box/zalo-reminder/use-send-zalo-reminder.js +98 -0
  77. package/components/chat-route/chat-box/zalo-reminder/zalo-reminder-content.js +294 -0
  78. package/components/chat-route/chat-box/zalo-rtf-utils.js +422 -0
  79. package/components/chat-route/chat-list/action-chat-item.js +116 -0
  80. package/components/chat-route/chat-list/assign-agent-dialog.js +170 -0
  81. package/components/chat-route/chat-list/assign-tag-dialog.js +242 -0
  82. package/components/chat-route/chat-list/chat-item-tags.js +105 -0
  83. package/components/chat-route/chat-list/chat-item.js +749 -0
  84. package/components/chat-route/chat-list/chat-list-item.js +370 -0
  85. package/components/chat-route/chat-list/chat-list-selection-bar.js +426 -0
  86. package/components/chat-route/chat-list/chat-search-preview.js +48 -0
  87. package/components/chat-route/chat-list/chat-sort-menu.js +89 -0
  88. package/components/chat-route/chat-list/edit-filter-modal.js +468 -0
  89. package/components/chat-route/chat-list/filter-chat-sources.js +222 -0
  90. package/components/chat-route/chat-list/filter-chat.js +501 -0
  91. package/components/chat-route/chat-list/filter-criteria-rows.js +845 -0
  92. package/components/chat-route/chat-list/filter-group-select.js +66 -0
  93. package/components/chat-route/chat-list/filter-pin-menu.js +165 -0
  94. package/components/chat-route/chat-list/friend-request/list-friend-request.js +287 -0
  95. package/components/chat-route/chat-list/index.js +243 -0
  96. package/components/chat-route/chat-list/inline-search-results.js +178 -0
  97. package/components/chat-route/chat-list/post-ids-field.js +104 -0
  98. package/components/chat-route/chat-list/quick-filter-bar.js +391 -0
  99. package/components/chat-route/chat-list/save-filter-popover.js +258 -0
  100. package/components/chat-route/chat-list/search-messages/index.js +225 -0
  101. package/components/chat-route/chat-list/search-result-header.js +51 -0
  102. package/components/chat-route/chat-list/util.js +604 -0
  103. package/components/chat-route/chat-toolbar.js +108 -0
  104. package/components/chat-route/hooks/use-reassign.js +111 -0
  105. package/components/chat-route/shared/close-conversation-dialog.js +111 -0
  106. package/components/chat-route/shared/force-transfer-dialog.js +109 -0
  107. package/components/chat-route/shared/in-queue-confirm-dialog.js +40 -0
  108. package/components/chat-route/util.js +146 -0
  109. package/components/chat-route/zalo/modal-action-zalo.js +76 -0
  110. package/components/chat-route/zalo/modal-add-friend-zalo.js +277 -0
  111. package/components/custom/SafeHTML.js +45 -0
  112. package/components/custom/audio-player.js +81 -0
  113. package/components/custom/avatar.js +95 -0
  114. package/components/custom/between-date-picker/index.js +86 -0
  115. package/components/custom/button-close.js +28 -0
  116. package/components/custom/button.js +80 -0
  117. package/components/custom/color-picker.js +81 -0
  118. package/components/custom/combobox.js +168 -0
  119. package/components/custom/custom-dialog.js +44 -0
  120. package/components/custom/custom-select.js +256 -0
  121. package/components/custom/cycle-value-helpers.js +11 -0
  122. package/components/custom/cycle-value.js +93 -0
  123. package/components/custom/date-time-picker/components/date-time-group.js +44 -0
  124. package/components/custom/date-time-picker/components/date-time-input.js +383 -0
  125. package/components/custom/date-time-picker/components/date-time-unit-input.js +138 -0
  126. package/components/custom/date-time-picker/components/month-year-picker.js +232 -0
  127. package/components/custom/date-time-picker/hooks/use-merge-refs.js +18 -0
  128. package/components/custom/date-time-picker/hooks/use-scroll-to-center.js +18 -0
  129. package/components/custom/date-time-picker/index.js +348 -0
  130. package/components/custom/field-file-upload/files-upload.js +87 -0
  131. package/components/custom/field-file-upload/image-upload.js +115 -0
  132. package/components/custom/field-file-upload/preview-images-modal.js +284 -0
  133. package/components/custom/field-file-upload/preview-list-file.js +134 -0
  134. package/components/custom/field-file-upload/preview-list-image.js +108 -0
  135. package/components/custom/field-file-upload/upload-custom.js +222 -0
  136. package/components/custom/field-generator.js +639 -0
  137. package/components/custom/field-layout-popover.js +304 -0
  138. package/components/custom/file-preview-modal.js +351 -0
  139. package/components/custom/html-edtior.js +100 -0
  140. package/components/custom/icon.js +49 -0
  141. package/components/custom/infinite-scroll-combobox.js +253 -0
  142. package/components/custom/inline-edit-popover.js +90 -0
  143. package/components/custom/inputs/calendar-input.js +0 -0
  144. package/components/custom/inputs/chip-input.js +136 -0
  145. package/components/custom/inputs/file-input.js +71 -0
  146. package/components/custom/inputs/mask-data/mask-data-input.js +80 -0
  147. package/components/custom/inputs/mask-data/mask-data-select.js +78 -0
  148. package/components/custom/inputs/mask-data/mask-data-textarea.js +75 -0
  149. package/components/custom/inputs/mask-data/reconcile-masked-edit.js +20 -0
  150. package/components/custom/jxl-decode.js +34 -0
  151. package/components/custom/jxl-image.js +68 -0
  152. package/components/custom/jxl-worker.js +12 -0
  153. package/components/custom/label.js +55 -0
  154. package/components/custom/layout.js +30 -0
  155. package/components/custom/multi-select-search-cc.js +179 -0
  156. package/components/custom/multiple-select-setting-tags.js +404 -0
  157. package/components/custom/multiple-selector.js +821 -0
  158. package/components/custom/pagination.js +95 -0
  159. package/components/custom/popup-confirm.js +120 -0
  160. package/components/custom/previews/facebook/facebook-appointment-booking-preview.js +83 -0
  161. package/components/custom/previews/facebook/facebook-audio-preview.js +130 -0
  162. package/components/custom/previews/facebook/facebook-event-preview.js +72 -0
  163. package/components/custom/previews/facebook/facebook-file-preview.js +95 -0
  164. package/components/custom/previews/facebook/facebook-group-preview.js +103 -0
  165. package/components/custom/previews/facebook/facebook-image-preview.js +81 -0
  166. package/components/custom/previews/facebook/facebook-page-preview.js +85 -0
  167. package/components/custom/previews/facebook/facebook-post-preview.js +77 -0
  168. package/components/custom/previews/facebook/facebook-reel-preview.js +83 -0
  169. package/components/custom/previews/facebook/facebook-sticker-preview.js +23 -0
  170. package/components/custom/previews/facebook/facebook-video-preview.js +114 -0
  171. package/components/custom/previews/facebook/index.js +24 -0
  172. package/components/custom/previews/file-preview.js +747 -0
  173. package/components/custom/previews/image-preview.js +288 -0
  174. package/components/custom/previews/instagram/index.js +4 -0
  175. package/components/custom/previews/instagram/instagram-share-post-preview.js +77 -0
  176. package/components/custom/previews/link-preview.js +166 -0
  177. package/components/custom/previews/product-preview.js +334 -0
  178. package/components/custom/previews/tiktok-order-preview.js +278 -0
  179. package/components/custom/previews/tiktok-product-preview.js +321 -0
  180. package/components/custom/previews/tiktok-voucher-preview.js +151 -0
  181. package/components/custom/previews/whatsapp/index.js +10 -0
  182. package/components/custom/previews/whatsapp/whatsapp-contact-card-preview.js +150 -0
  183. package/components/custom/previews/whatsapp/whatsapp-order-preview.js +38 -0
  184. package/components/custom/previews/whatsapp/whatsapp-poll-preview.js +29 -0
  185. package/components/custom/previews/whatsapp/whatsapp-product-preview.js +29 -0
  186. package/components/custom/retry-image.js +44 -0
  187. package/components/custom/search-input.js +26 -0
  188. package/components/custom/separator-with-text.js +14 -0
  189. package/components/custom/showToast.js +21 -0
  190. package/components/custom/sortable-header.js +62 -0
  191. package/components/custom/table-resize-line.js +92 -0
  192. package/components/custom/time-picker/index.js +73 -0
  193. package/components/custom/time-picker/time-picker-input.js +159 -0
  194. package/components/custom/time-picker/time-picker-utils.js +152 -0
  195. package/components/custom/viewers/audio-player.js +192 -0
  196. package/components/custom/viewers/code-viewer.js +140 -0
  197. package/components/custom/viewers/csv-viewer.js +174 -0
  198. package/components/custom/viewers/docx-viewer.js +145 -0
  199. package/components/custom/viewers/image-viewer.js +104 -0
  200. package/components/custom/viewers/markdown-viewer.js +124 -0
  201. package/components/custom/viewers/pdf-viewer.js +333 -0
  202. package/components/custom/viewers/spreadsheet-viewer.js +232 -0
  203. package/components/custom/viewers/unsupported-viewer.js +85 -0
  204. package/components/custom/viewers/video-player.js +241 -0
  205. package/components/customers-route/change-owner-modal.js +430 -0
  206. package/components/customers-route/contact-note-button.js +212 -0
  207. package/components/customers-route/customer-source.js +37 -0
  208. package/components/customers-route/table/columns.js +675 -0
  209. package/components/customers-route/table/edit-fields.js +125 -0
  210. package/components/customers-route/table/editable-cell.js +218 -0
  211. package/components/customers-route/table/note-cell.js +77 -0
  212. package/components/dialogs/invite-agent.js +677 -0
  213. package/components/error-boundary.js +53 -0
  214. package/components/leads-route/duplicate-phone-warning-toast.js +96 -0
  215. package/components/leads-route/table/columns.js +523 -0
  216. package/components/leads-route/table/contact-cell-name.js +82 -0
  217. package/components/leads-route/table/edit-fields.js +142 -0
  218. package/components/leads-route/table/editable-cell.js +228 -0
  219. package/components/org-tree/group-name-display.js +36 -0
  220. package/components/org-tree/group-tree-multi-select.js +208 -0
  221. package/components/org-tree/group-tree-select.js +220 -0
  222. package/components/posts-route/dialog-select-posts.js +284 -0
  223. package/components/reports-route/agents/multi-select-agents.js +55 -0
  224. package/components/reports-route/agents/multi-select-search-agents.js +43 -0
  225. package/components/settings-route/chat-layout/detail/constants.js +353 -0
  226. package/components/settings-route/chat-layout/detail/form.js +43 -0
  227. package/components/settings-route/chat-timeout/channel-multi-select.js +119 -0
  228. package/components/settings-route/multi-channel/facebook/hooks/use-facebook-sdk.js +3 -0
  229. package/components/settings-route/object-rule/util.js +79 -0
  230. package/components/settings-route/routing/settings-dialog.js +561 -0
  231. package/components/settings-route/switchboard/queue-new/constant.js +9 -0
  232. package/components/shared/linked-customer-fields/LinkedCustomerFields.js +91 -0
  233. package/components/shared/linked-customer-fields/use-linked-customer-fields.js +123 -0
  234. package/components/smartqc-route/qc-form/controls/multi-select-search-agents.js +50 -0
  235. package/components/ui/alert-dialog.js +112 -0
  236. package/components/ui/avatar.js +44 -0
  237. package/components/ui/badge.js +54 -0
  238. package/components/ui/button.js +48 -0
  239. package/components/ui/calendar.js +70 -0
  240. package/components/ui/card.js +58 -0
  241. package/components/ui/carousel.js +238 -0
  242. package/components/ui/checkbox.js +27 -0
  243. package/components/ui/command.js +111 -0
  244. package/components/ui/context-menu.js +154 -0
  245. package/components/ui/dialog.js +104 -0
  246. package/components/ui/dropdown-menu.js +169 -0
  247. package/components/ui/form.js +104 -0
  248. package/components/ui/input.js +115 -0
  249. package/components/ui/label.js +20 -0
  250. package/components/ui/popover.js +29 -0
  251. package/components/ui/radio-group.js +53 -0
  252. package/components/ui/scroll-area.js +38 -0
  253. package/components/ui/select.js +147 -0
  254. package/components/ui/separator.js +24 -0
  255. package/components/ui/sheet.js +113 -0
  256. package/components/ui/skeleton.js +17 -0
  257. package/components/ui/slider.js +26 -0
  258. package/components/ui/sonner.js +76 -0
  259. package/components/ui/switch.js +54 -0
  260. package/components/ui/table-v2.js +269 -0
  261. package/components/ui/table.js +245 -0
  262. package/components/ui/tabs.js +89 -0
  263. package/components/ui/textarea.js +33 -0
  264. package/components/ui/tooltip.js +40 -0
  265. package/components/workflow-editor/nodes/ticket/picker-field.js +233 -0
  266. package/components/zalo-login-qr.js +237 -0
  267. package/components/zalo-login-status-modal.js +45 -0
  268. package/config/env.js +27 -0
  269. package/config/hmr.js +8 -0
  270. package/config/navigation.js +54 -0
  271. package/config/runtime.js +46 -0
  272. package/config/theme.js +274 -0
  273. package/constants/brand-colors.js +32 -0
  274. package/constants/chat-icon.js +34 -0
  275. package/dist/chat.css +1 -0
  276. package/exceptions/throw-exception.js +15 -0
  277. package/hooks/use-chat-attachments.js +106 -0
  278. package/hooks/use-chat-configs.js +62 -0
  279. package/hooks/use-chat-customer.js +33 -0
  280. package/hooks/use-chat-display-config-sync.js +72 -0
  281. package/hooks/use-chat-permission.js +68 -0
  282. package/hooks/use-chat-room-detail.js +44 -0
  283. package/hooks/use-chat-selection.js +26 -0
  284. package/hooks/use-chat-sort.js +69 -0
  285. package/hooks/use-chat-surface.js +8 -0
  286. package/hooks/use-common-filter-options.js +70 -0
  287. package/hooks/use-crm-organization-filter-options.js +0 -0
  288. package/hooks/use-crm-organizations-data.js +23 -0
  289. package/hooks/use-debounce.js +14 -0
  290. package/hooks/use-dynamic-fields.js +7 -0
  291. package/hooks/use-facebook-notice.js +24 -0
  292. package/hooks/use-feature-flags.js +6 -0
  293. package/hooks/use-field-layout-override.js +46 -0
  294. package/hooks/use-file-upload.js +119 -0
  295. package/hooks/use-file-viewer.js +93 -0
  296. package/hooks/use-frozen-columns.js +29 -0
  297. package/hooks/use-horizontal-scroll.js +41 -0
  298. package/hooks/use-image-preview-queue.js +101 -0
  299. package/hooks/use-inline-editable-cell.js +97 -0
  300. package/hooks/use-intersection-observer.js +24 -0
  301. package/hooks/use-interval.js +20 -0
  302. package/hooks/use-latest-message-id.js +13 -0
  303. package/hooks/use-lead-filter-options.js +0 -0
  304. package/hooks/use-logout-listener.js +43 -0
  305. package/hooks/use-org-tree.js +43 -0
  306. package/hooks/use-permission.js +33 -0
  307. package/hooks/use-quick-filters.js +175 -0
  308. package/hooks/use-resizable-table.js +0 -0
  309. package/hooks/use-setting-common.js +389 -0
  310. package/hooks/use-settings.js +41 -0
  311. package/hooks/use-sort-bindings.js +24 -0
  312. package/hooks/use-table-display-config.js +0 -0
  313. package/hooks/use-tenant-config.js +99 -0
  314. package/hooks/use-update-user-config.js +150 -0
  315. package/hooks/use-zalo-connection.js +57 -0
  316. package/hooks/use-zalo-mention-list.js +31 -0
  317. package/index.css +624 -0
  318. package/index.js +100 -0
  319. package/lib/deal-name.js +27 -0
  320. package/lib/i18n/react.js +7 -0
  321. package/lib/i18n/use-language.js +0 -0
  322. package/lib/query-client.js +34 -0
  323. package/lib/utils.js +25 -0
  324. package/package.json +108 -0
  325. package/provider/chat-context.js +9 -0
  326. package/provider/chat-provider.js +177 -0
  327. package/provider/chat-room-context.js +9 -0
  328. package/provider/chat-room-hooks.js +8 -0
  329. package/provider/chat-room-provider.js +84 -0
  330. package/provider/hooks.js +22 -0
  331. package/provider/index.js +42 -0
  332. package/public/contact-card-user.svg +25 -0
  333. package/public/images/icons/facebook.svg +1 -0
  334. package/public/images/icons/instagram.svg +1 -0
  335. package/public/images/icons/shopee.svg +1 -0
  336. package/public/images/icons/whatsapp.svg +1 -0
  337. package/public/images/icons/zalo.png +0 -0
  338. package/public/images/images.jpg +0 -0
  339. package/public/images/ticket_sources/api.png +0 -0
  340. package/public/images/ticket_sources/email.png +0 -0
  341. package/public/images/ticket_sources/facebook.png +0 -0
  342. package/public/images/ticket_sources/hand.png +0 -0
  343. package/public/images/ticket_sources/instagram.png +0 -0
  344. package/public/images/ticket_sources/livechat.png +0 -0
  345. package/public/images/ticket_sources/shopee.svg +1 -0
  346. package/public/images/ticket_sources/sms.png +0 -0
  347. package/public/images/ticket_sources/telephone-call-in.png +0 -0
  348. package/public/images/ticket_sources/telephone-call-out.png +0 -0
  349. package/public/images/ticket_sources/tiktok.png +0 -0
  350. package/public/images/ticket_sources/tiktok_shop.png +0 -0
  351. package/public/images/ticket_sources/whatsapp.png +0 -0
  352. package/public/images/ticket_sources/whatsapp.svg +1 -0
  353. package/public/images/ticket_sources/zalo.png +0 -0
  354. package/public/placeholder.svg +13 -0
  355. package/store/app-status-store.js +17 -0
  356. package/store/chat-display-config-store.js +182 -0
  357. package/store/chat-display-config.resolvers.js +88 -0
  358. package/store/chat-store-helpers.js +62 -0
  359. package/store/chat-store.js +931 -0
  360. package/store/crm-organization-list-filter-store.js +126 -0
  361. package/store/customer-list-filter-store.js +139 -0
  362. package/store/devtools.js +9 -0
  363. package/store/duplicate-phone-warning-store.js +18 -0
  364. package/store/entity-extract-store.js +72 -0
  365. package/store/file-preview-store.js +27 -0
  366. package/store/filter-store.js +289 -0
  367. package/store/index.js +22 -0
  368. package/store/knowledge-crawl-store.js +53 -0
  369. package/store/lead-list-filter-store.js +142 -0
  370. package/store/list-pagination-state.js +44 -0
  371. package/store/lumi-learn-from-chat-store.js +69 -0
  372. package/store/merge-suggestion-store.js +27 -0
  373. package/store/notification-store.js +63 -0
  374. package/store/preview-image-store.js +22 -0
  375. package/store/settings-store/multi-channel/email-list-store.js +20 -0
  376. package/store/settings-store/multi-channel/facebook-list-store.js +22 -0
  377. package/store/settings-store/multi-channel/google-list-store.js +20 -0
  378. package/store/settings-store/multi-channel/instagram-list-store.js +20 -0
  379. package/store/settings-store/multi-channel/misa-list-store.js +20 -0
  380. package/store/settings-store/multi-channel/nhanhvn-list-store.js +20 -0
  381. package/store/settings-store/multi-channel/shopee-list-store.js +20 -0
  382. package/store/settings-store/multi-channel/shopee-order-list-store.js +20 -0
  383. package/store/settings-store/multi-channel/sms-list-store.js +20 -0
  384. package/store/settings-store/multi-channel/tiktok-business-list-store.js +20 -0
  385. package/store/settings-store/multi-channel/tiktok-list-store.js +20 -0
  386. package/store/settings-store/multi-channel/zalo-list-store.js +22 -0
  387. package/store/settings-store/multi-channel/zalo-user-list-store.js +25 -0
  388. package/store/smart-qc-store/smart-qc-list-filter-store.js +70 -0
  389. package/store/tenant-config-store.js +21 -0
  390. package/store/user-detail-store.js +17 -0
  391. package/store/user-status-store.js +33 -0
  392. package/store/workflow-store.js +24 -0
  393. package/store/workspace-store.js +32 -0
  394. package/tailwind-preset.js +129 -0
  395. package/utils/chat-actions.js +414 -0
  396. package/utils/chat_socket.js +1225 -0
  397. package/utils/extra-columns.js +108 -0
  398. package/utils/file.js +316 -0
  399. package/utils/html-to-plain-text.js +12 -0
  400. package/utils/notification.js +71 -0
  401. package/utils/replaceZaloSprite.js +64 -0
  402. package/utils/show-be-error.js +17 -0
  403. package/utils/typing_socket.js +101 -0
@@ -0,0 +1,995 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
11
+ import { useVirtualizer } from "@tanstack/react-virtual";
12
+ import { ChevronDown, ChevronsUp } from "lucide-react";
13
+ import { useShallow } from "zustand/shallow";
14
+ import ChatMessage from "./chat-message.js";
15
+ import useChatStore from "../../../store/chat-store.js";
16
+ import { cn } from "../../../lib/utils.js";
17
+ import { getUser, checkIsViewAsMode } from "@zenify-omni/chat-js/utils/auth.js";
18
+ import SeparatorWithText from "../../custom/separator-with-text.js";
19
+ import { navigate } from "../../../config/navigation.js";
20
+ import QUERY_KEY from "@zenify-omni/chat-js/constants/use-query-key.js";
21
+ import { CreateZaloReminder } from "./zalo-reminder/create-zalo-reminder.js";
22
+ import { MessageType, ActionMessageTypes } from "@zenify-omni/chat-js/constants/chat-message.js";
23
+ import { useSendZaloReminder } from "./zalo-reminder/use-send-zalo-reminder.js";
24
+ import { getChatMessageList, markRoomRead } from "@zenify-omni/chat-js/api/chatroom.js";
25
+ import { ChatFeedback } from "./chat-feedback/index.js";
26
+ import { ChatInsight } from "./chat-feedback/chat-insight.js";
27
+ import ForwardMessageModal from "./forward-message-modal.js";
28
+ import PinnedMessages from "./pinned-messages.js";
29
+ import { getPinnedMessages } from "@zenify-omni/chat-js/api/chat.js";
30
+ import { isZaloPersonal } from "../util.js";
31
+ import { useGetZaloAllocatedList } from "../../../hooks/use-setting-common.js";
32
+ import { useTranslation } from "react-i18next";
33
+ const REQUEST_LIMIT = 30;
34
+ const AT_BOTTOM_THRESHOLD = 80;
35
+ const AT_TOP_THRESHOLD = 80;
36
+ const ACTION_LINE_SIZE = 40;
37
+ const DEFAULT_MESSAGE_SIZE = 80;
38
+ const ESTIMATED_SIZE_BY_TYPE = {
39
+ // Media: bubble cao do ảnh/video
40
+ [MessageType.IMAGE]: 280,
41
+ [MessageType.IMAGES]: 280,
42
+ [MessageType.VIDEO]: 280,
43
+ // Card bản đồ
44
+ [MessageType.LOCATION]: 200,
45
+ // Sticker
46
+ [MessageType.STICKER]: 160,
47
+ // Link preview (thumbnail + tiêu đề + mô tả)
48
+ [MessageType.LINK]: 140,
49
+ // Tin nhắn tiện ích / template
50
+ [MessageType.UTILITY_MESSAGES]: 120,
51
+ // Card nhắc hẹn (nội dung + thời gian)
52
+ [MessageType.CREATE_REMINDER]: 110,
53
+ [MessageType.UPDATE_REMINDER]: 110,
54
+ // Card đính kèm: file / danh thiếp / sản phẩm / lời mời kết bạn
55
+ [MessageType.FILE]: 90,
56
+ [MessageType.CONTACT_CARD]: 90,
57
+ [MessageType.PRODUCT]: 90,
58
+ [MessageType.FRIEND_REQUEST]: 90,
59
+ // Text / fallback
60
+ [MessageType.TEXT]: DEFAULT_MESSAGE_SIZE,
61
+ [MessageType.FALLBACK]: DEFAULT_MESSAGE_SIZE,
62
+ // Audio / cuộc gọi Zalo
63
+ [MessageType.AUDIO]: 60,
64
+ [MessageType.ZALO_CALL]: 60,
65
+ // Không hỗ trợ: render dòng ngắn
66
+ [MessageType.UNSUPPORTED]: 60
67
+ };
68
+ const estimateMessageSize = (message) => {
69
+ if (message?.isUnreadSeparator) return 44;
70
+ const type = message?.content?.type;
71
+ const base = ActionMessageTypes.includes(type) ? ACTION_LINE_SIZE : ESTIMATED_SIZE_BY_TYPE[type] ?? DEFAULT_MESSAGE_SIZE;
72
+ return message?.dateLabel ? base + 32 : base;
73
+ };
74
+ function ChatMessageList({
75
+ chatRoomId,
76
+ isChatAllowed,
77
+ chatRoomDetailReady,
78
+ className,
79
+ chatRoom,
80
+ ticket,
81
+ requester,
82
+ isZaloConnected,
83
+ // Additive, opt-in hooks for reuse outside the /chat route (e.g. Lumi).
84
+ // Defaults preserve the exact /chat behaviour.
85
+ redirectOnError = true,
86
+ renderMessageExtra,
87
+ // Optional override for the feedback dialog (defaults to the legacy ChatFeedback).
88
+ // Receives { open, onOpenChange, message }. Lumi plugs in its own real-submit form.
89
+ renderFeedbackDialog,
90
+ // Extra bottom padding for the scroll container (e.g. when an overlay notice sits above the input).
91
+ scrollPaddingBottom
92
+ }) {
93
+ const { t } = useTranslation();
94
+ const queryClient = useQueryClient();
95
+ const user = getUser();
96
+ const { data: zaloAllocatedList } = useGetZaloAllocatedList(user);
97
+ const [
98
+ ticketChatMessages,
99
+ setTicketChatMessages,
100
+ setCurrentChatId,
101
+ findReplySelected,
102
+ setFindReplySelected,
103
+ setHighlightMessageId,
104
+ setTokenForGetMessageApi,
105
+ feedbackMessage,
106
+ insightMessage,
107
+ forwardMessage,
108
+ setFeedbackMessage,
109
+ setInsightMessage,
110
+ setForwardMessage,
111
+ findPinSelected,
112
+ setFindPinSelected,
113
+ isSelectMode,
114
+ selectedMessages,
115
+ toggleSelectedMessage,
116
+ setSelectMode,
117
+ markRoomLoaded,
118
+ updateChatUnread
119
+ ] = useChatStore(
120
+ useShallow((state) => [
121
+ state.ticketChatMessages,
122
+ state.setTicketChatMessages,
123
+ state.setCurrentChatId,
124
+ state.findReplySelected,
125
+ state.setFindReplySelected,
126
+ state.setHighlightMessageId,
127
+ state.setTokenForGetMessageApi,
128
+ state.feedbackMessage,
129
+ state.insightMessage,
130
+ state.forwardMessage,
131
+ state.setFeedbackMessage,
132
+ state.setInsightMessage,
133
+ state.setForwardMessage,
134
+ state.findPinSelected,
135
+ state.setFindPinSelected,
136
+ state.isSelectMode,
137
+ state.selectedMessages,
138
+ state.toggleSelectedMessage,
139
+ state.setSelectMode,
140
+ state.markRoomLoaded,
141
+ state.updateChatUnread
142
+ ])
143
+ );
144
+ const isRoomLoaded = useChatStore(
145
+ (state) => !!state.loadedRooms?.[chatRoomId]
146
+ );
147
+ const { data: pinnedData } = useQuery({
148
+ queryKey: [QUERY_KEY.GET_PINNED_MESSAGES, chatRoomId, isZaloConnected],
149
+ queryFn: async () => {
150
+ const { data } = await getPinnedMessages(chatRoomId);
151
+ return data;
152
+ },
153
+ enabled: !!chatRoomId && isZaloPersonal(ticket) && isZaloConnected
154
+ });
155
+ const [isReminderDialogOpen, setIsReminderDialogOpen] = useState(false);
156
+ const [reminderData, setReminderData] = useState(null);
157
+ const [isAtBottom, setIsAtBottom] = useState(true);
158
+ const [remainingUnread, setRemainingUnread] = useState(0);
159
+ const [, setIsScrollingUp] = useState(false);
160
+ const [unreadConsumed, setUnreadConsumed] = useState(false);
161
+ const [isSeparatorInViewport, setIsSeparatorInViewport] = useState(false);
162
+ const [hasNewMsgWhileAway, setHasNewMsgWhileAway] = useState(false);
163
+ const [separatorHighlighted, setSeparatorHighlighted] = useState(false);
164
+ const separatorHighlightTimerRef = useRef(null);
165
+ const [isInitialBatch, setIsInitialBatch] = useState(false);
166
+ const hasAnimatedRef = useRef(false);
167
+ const batchAnimationTimerRef = useRef(null);
168
+ const shouldAnimateRef = useRef(false);
169
+ const [barProgress, setBarProgress] = useState(0);
170
+ const [barVisible, setBarVisible] = useState(false);
171
+ const barTimerRef = useRef(null);
172
+ const chatMessages = useMemo(
173
+ () => ticketChatMessages[chatRoomId] || [],
174
+ [ticketChatMessages, chatRoomId]
175
+ );
176
+ const ticketSource = useMemo(() => ticket?.source, [ticket]);
177
+ const containerRef = useRef(null);
178
+ const innerRef = useRef(null);
179
+ const roomIdRef = useRef(null);
180
+ const bottomMsgIdRef = useRef(null);
181
+ const wasAtBottomRef = useRef(true);
182
+ const loadingOlderRef = useRef(false);
183
+ const pendingAnchorRef = useRef(null);
184
+ const prevDistanceFromBottomRef = useRef(0);
185
+ const scrollAnchorRef = useRef(null);
186
+ const virtualItemsRef = useRef([]);
187
+ const isClearingRef = useRef(false);
188
+ const separatorTimerRef = useRef(null);
189
+ const prevRoomIdRef = useRef(null);
190
+ const firstNewMsgBoundaryRef = useRef(null);
191
+ const prevScrollTopRef = useRef(0);
192
+ const skipNextPaginationRef = useRef(false);
193
+ const hasInteractedRef = useRef(false);
194
+ const sepIdxRef = useRef(-1);
195
+ const scrollRafPendingRef = useRef(false);
196
+ const frameHadUserScrollRef = useRef(false);
197
+ const getItemKey = useCallback(
198
+ (index) => chatMessages[index]?._id,
199
+ [chatMessages]
200
+ );
201
+ const estimateSize = useCallback(
202
+ (index) => estimateMessageSize(chatMessages[index]),
203
+ [chatMessages]
204
+ );
205
+ const virtualizer = useVirtualizer({
206
+ count: chatMessages.length,
207
+ getScrollElement: () => containerRef.current,
208
+ estimateSize,
209
+ overscan: 16,
210
+ getItemKey
211
+ });
212
+ const virtualItems = virtualizer.getVirtualItems();
213
+ virtualItemsRef.current = virtualItems;
214
+ const scrollToBottom = useCallback(
215
+ (behavior = "auto") => {
216
+ const last = chatMessages.length - 1;
217
+ if (last < 0) return;
218
+ virtualizer.scrollToIndex(last, { align: "end", behavior });
219
+ requestAnimationFrame(() => {
220
+ virtualizer.scrollToIndex(last, { align: "end", behavior: "auto" });
221
+ const el = containerRef.current;
222
+ if (el) el.scrollTop = el.scrollHeight;
223
+ });
224
+ },
225
+ [chatMessages.length, virtualizer]
226
+ );
227
+ const scrollToUnreadOrBottom = useCallback(
228
+ (behavior = "auto") => {
229
+ const sepIdx = chatMessages.findIndex((m) => m.isUnreadSeparator);
230
+ if (sepIdx !== -1) {
231
+ virtualizer.scrollToIndex(sepIdx, { align: "start", behavior });
232
+ if (behavior !== "smooth") {
233
+ requestAnimationFrame(() => {
234
+ virtualizer.scrollToIndex(sepIdx, {
235
+ align: "start",
236
+ behavior: "auto"
237
+ });
238
+ });
239
+ }
240
+ } else {
241
+ scrollToBottom(behavior);
242
+ }
243
+ },
244
+ [chatMessages, virtualizer, scrollToBottom]
245
+ );
246
+ const computeRemainingUnread = useCallback(() => {
247
+ const el = containerRef.current;
248
+ if (!el) return;
249
+ const sepIdx = sepIdxRef.current;
250
+ if (sepIdx === -1) {
251
+ setRemainingUnread(0);
252
+ return;
253
+ }
254
+ const msgs = useChatStore.getState().ticketChatMessages[chatRoomId] || [];
255
+ const totalUnread = msgs.length - sepIdx - 1;
256
+ const scrollBottom = el.scrollTop + el.clientHeight;
257
+ const seenCount = virtualItemsRef.current.filter(
258
+ (item) => item.index > sepIdx && item.start < scrollBottom
259
+ ).length;
260
+ const remaining = Math.max(0, totalUnread - seenCount);
261
+ setRemainingUnread(remaining);
262
+ if (remaining === 0 && hasInteractedRef.current) setUnreadConsumed(true);
263
+ }, [chatRoomId]);
264
+ const computeSeparatorVisibility = useCallback(() => {
265
+ const el = containerRef.current;
266
+ if (!el) return;
267
+ const sepIdx = sepIdxRef.current;
268
+ if (sepIdx === -1) {
269
+ setIsSeparatorInViewport(false);
270
+ return;
271
+ }
272
+ const sepItem = virtualItemsRef.current.find(
273
+ (item) => item.index === sepIdx
274
+ );
275
+ if (!sepItem) {
276
+ setIsSeparatorInViewport(false);
277
+ return;
278
+ }
279
+ const inView = sepItem.start < el.scrollTop + el.clientHeight && sepItem.start + sepItem.size > el.scrollTop;
280
+ setIsSeparatorInViewport(inView);
281
+ if (inView && hasInteractedRef.current) setUnreadConsumed(true);
282
+ }, []);
283
+ useLayoutEffect(() => {
284
+ sepIdxRef.current = chatMessages.findIndex((m) => m.isUnreadSeparator);
285
+ }, [chatMessages]);
286
+ const { refetch, isRefetching } = useQuery({
287
+ queryKey: [QUERY_KEY.GET_CHAT_MESSAGES, { ticketId: chatRoomId }],
288
+ queryFn: async ({ signal }) => {
289
+ const currentToken = useChatStore.getState().tokenForGetMessageApi[chatRoomId] || null;
290
+ const roomUnreadHint = !currentToken ? useChatStore.getState().chatList.find((x) => x._id === chatRoomId)?.unread ?? 0 : 0;
291
+ const limit = Math.min(
292
+ 200,
293
+ Math.max(REQUEST_LIMIT, roomUnreadHint + REQUEST_LIMIT)
294
+ );
295
+ const isSpy = checkIsViewAsMode();
296
+ const params = {
297
+ limit,
298
+ sort: "-created_at -_id",
299
+ type: "cursor",
300
+ cursor: currentToken,
301
+ ...currentToken ? { ignore_state: true } : {},
302
+ ...useChatStore.getState().isChatAllowed ? { isChatAllowed: true } : {},
303
+ ...isSpy ? { isSpy: true } : {}
304
+ };
305
+ try {
306
+ const { data } = await getChatMessageList(chatRoomId, {
307
+ params,
308
+ signal
309
+ });
310
+ if (signal.aborted) return [];
311
+ setTokenForGetMessageApi(chatRoomId, data?.next_cursor ?? null);
312
+ const { items } = data;
313
+ let readItems = [...items.reverse()];
314
+ const effectiveUnread = data.unread_count > 0 ? data.unread_count : roomUnreadHint;
315
+ const alreadyHasSeparator = (useChatStore.getState().ticketChatMessages[chatRoomId] || []).some((m) => m.isUnreadSeparator);
316
+ if (!currentToken && effectiveUnread > 0 && !alreadyHasSeparator) {
317
+ const separator = {
318
+ _id: `unread-separator-${chatRoomId}`,
319
+ isUnreadSeparator: true,
320
+ unreadCount: effectiveUnread,
321
+ normalized: true
322
+ };
323
+ const readCount = Math.max(0, readItems.length - effectiveUnread);
324
+ const readPart = readItems.slice(0, readCount);
325
+ separator.created_at = readPart[readPart.length - 1]?.created_at ?? (/* @__PURE__ */ new Date(0)).toISOString();
326
+ readItems = [...readPart, separator, ...readItems.slice(readCount)];
327
+ }
328
+ const latestMessages = useChatStore.getState().ticketChatMessages[chatRoomId] || [];
329
+ const newChatMessages = [...readItems, ...latestMessages];
330
+ if ((!data?.next_cursor || !items.length) && newChatMessages[0]) {
331
+ newChatMessages[0].isFirstMessage = true;
332
+ }
333
+ setTicketChatMessages(chatRoomId, newChatMessages);
334
+ if (useChatStore.getState().ticketChatMessages[chatRoomId]) {
335
+ markRoomLoaded(chatRoomId);
336
+ }
337
+ if (!currentToken && !isSpy && useChatStore.getState().isChatAllowed) {
338
+ useChatStore.getState().updateChatUnread(chatRoomId, { unread: 0 });
339
+ markRoomRead(chatRoomId, { isChatAllowed: true }).catch(() => {
340
+ });
341
+ }
342
+ } catch {
343
+ if (redirectOnError) {
344
+ setCurrentChatId(null);
345
+ navigate({ to: "/chat" });
346
+ }
347
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_CHAT_LIST] });
348
+ }
349
+ return [];
350
+ },
351
+ enabled: false
352
+ });
353
+ const handleScrollToMessage = (messageId) => {
354
+ const idx = chatMessages.findIndex((m) => m._id === messageId);
355
+ if (idx < 0) return;
356
+ virtualizer.scrollToIndex(idx, { align: "center" });
357
+ requestAnimationFrame(
358
+ () => virtualizer.scrollToIndex(idx, { align: "center" })
359
+ );
360
+ setHighlightMessageId(messageId);
361
+ setTimeout(() => {
362
+ setHighlightMessageId(null);
363
+ }, 3e3);
364
+ };
365
+ const fetchAndScrollTo = async (messageId, fetchingRef, clearState) => {
366
+ const existing = chatMessages?.find((e) => e._id === messageId);
367
+ if (existing) {
368
+ fetchingRef.current = false;
369
+ clearState(null);
370
+ handleScrollToMessage(existing._id);
371
+ return;
372
+ }
373
+ if (fetchingRef.current) return;
374
+ fetchingRef.current = true;
375
+ try {
376
+ const params = {
377
+ limit: 1e3,
378
+ sort: "-created_at -_id",
379
+ type: "cursor",
380
+ ignore_state: true,
381
+ until_id: messageId
382
+ };
383
+ const { data } = await getChatMessageList(chatRoomId, { params });
384
+ const { items } = data;
385
+ if (!items?.length) {
386
+ fetchingRef.current = false;
387
+ clearState(null);
388
+ return;
389
+ }
390
+ const latestMessages = useChatStore.getState().ticketChatMessages[chatRoomId] || [];
391
+ const newChatMessages = [...items.reverse(), ...latestMessages];
392
+ setTicketChatMessages(chatRoomId, newChatMessages);
393
+ } catch {
394
+ fetchingRef.current = false;
395
+ clearState(null);
396
+ }
397
+ };
398
+ const replyFetchingRef = useRef(false);
399
+ useEffect(() => {
400
+ if (!findReplySelected) return;
401
+ fetchAndScrollTo(findReplySelected, replyFetchingRef, setFindReplySelected);
402
+ }, [findReplySelected, chatMessages]);
403
+ const pinFetchingRef = useRef(false);
404
+ useEffect(() => {
405
+ if (!findPinSelected) return;
406
+ fetchAndScrollTo(findPinSelected, pinFetchingRef, setFindPinSelected);
407
+ }, [findPinSelected, chatMessages]);
408
+ useEffect(() => {
409
+ if (!chatRoomDetailReady) return;
410
+ if (useChatStore.getState().loadedRooms[chatRoomId]) {
411
+ const currentUnread = useChatStore.getState().chatList.find((c) => c._id === chatRoomId)?.unread ?? 0;
412
+ const cachedMsgs = useChatStore.getState().ticketChatMessages[chatRoomId] || [];
413
+ const msgsWithoutSep = cachedMsgs.filter((m) => !m.isUnreadSeparator);
414
+ if (currentUnread > 0 && currentUnread >= msgsWithoutSep.length) {
415
+ if (!checkIsViewAsMode() && useChatStore.getState().isChatAllowed) {
416
+ useChatStore.getState().updateChatUnread(chatRoomId, { unread: 0 });
417
+ }
418
+ setTokenForGetMessageApi(chatRoomId, null);
419
+ refetch();
420
+ return;
421
+ }
422
+ if (currentUnread > 0 && !checkIsViewAsMode() && useChatStore.getState().isChatAllowed) {
423
+ useChatStore.getState().updateChatUnread(chatRoomId, { unread: 0 });
424
+ markRoomRead(chatRoomId, { isChatAllowed: true }).catch(() => {
425
+ });
426
+ }
427
+ if (currentUnread > 0 && msgsWithoutSep.length > 0) {
428
+ const separator = {
429
+ _id: `unread-separator-${chatRoomId}`,
430
+ isUnreadSeparator: true,
431
+ unreadCount: currentUnread,
432
+ normalized: true
433
+ };
434
+ const readCount = Math.max(0, msgsWithoutSep.length - currentUnread);
435
+ const readPart = msgsWithoutSep.slice(0, readCount);
436
+ separator.created_at = readPart[readPart.length - 1]?.created_at ?? (/* @__PURE__ */ new Date(0)).toISOString();
437
+ setTicketChatMessages(chatRoomId, [
438
+ ...readPart,
439
+ separator,
440
+ ...msgsWithoutSep.slice(readCount)
441
+ ]);
442
+ } else if (msgsWithoutSep.length !== cachedMsgs.length) {
443
+ setTicketChatMessages(chatRoomId, msgsWithoutSep);
444
+ }
445
+ return;
446
+ }
447
+ setTokenForGetMessageApi(chatRoomId, null);
448
+ refetch();
449
+ }, [chatRoomId, chatRoomDetailReady]);
450
+ useEffect(() => {
451
+ const handleVisibilityChange = () => {
452
+ if (document.visibilityState !== "hidden") return;
453
+ const state = useChatStore.getState();
454
+ const roomId = state.currentChatId;
455
+ if (!roomId || !state.isChatAllowed || checkIsViewAsMode()) return;
456
+ markRoomRead(roomId, { isChatAllowed: true }).catch(() => {
457
+ });
458
+ };
459
+ document.addEventListener("visibilitychange", handleVisibilityChange);
460
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
461
+ }, []);
462
+ useEffect(() => {
463
+ const element = containerRef.current;
464
+ if (!element) return;
465
+ let scrollRafId = null;
466
+ const onScroll = () => {
467
+ const isProgrammatic = skipNextPaginationRef.current;
468
+ if (isProgrammatic) skipNextPaginationRef.current = false;
469
+ else frameHadUserScrollRef.current = true;
470
+ if (scrollRafPendingRef.current) return;
471
+ scrollRafPendingRef.current = true;
472
+ scrollRafId = requestAnimationFrame(() => {
473
+ scrollRafPendingRef.current = false;
474
+ const frameIsUserInitiated = frameHadUserScrollRef.current;
475
+ frameHadUserScrollRef.current = false;
476
+ const isGoingUp = element.scrollTop < prevScrollTopRef.current;
477
+ prevScrollTopRef.current = element.scrollTop;
478
+ const atBottom = element.scrollHeight - element.scrollTop - element.clientHeight < AT_BOTTOM_THRESHOLD;
479
+ wasAtBottomRef.current = atBottom;
480
+ setIsAtBottom(atBottom);
481
+ if (atBottom) setHasNewMsgWhileAway(false);
482
+ computeRemainingUnread();
483
+ computeSeparatorVisibility();
484
+ setIsScrollingUp(isGoingUp);
485
+ if (!atBottom && pendingAnchorRef.current == null) {
486
+ const scrollTop = element.scrollTop;
487
+ const firstVisible = virtualItemsRef.current.find(
488
+ (item) => item.start + item.size > scrollTop
489
+ );
490
+ if (firstVisible) {
491
+ scrollAnchorRef.current = {
492
+ key: firstVisible.key,
493
+ offset: scrollTop - firstVisible.start
494
+ };
495
+ }
496
+ }
497
+ if (!frameIsUserInitiated) return;
498
+ hasInteractedRef.current = true;
499
+ if (element.scrollTop > AT_TOP_THRESHOLD || loadingOlderRef.current)
500
+ return;
501
+ const messages = useChatStore.getState().ticketChatMessages[chatRoomId] || [];
502
+ if (!messages.length || messages[0]?.isFirstMessage) return;
503
+ loadingOlderRef.current = true;
504
+ pendingAnchorRef.current = messages[0]?._id;
505
+ prevDistanceFromBottomRef.current = element.scrollHeight - element.scrollTop;
506
+ refetch().finally(() => {
507
+ loadingOlderRef.current = false;
508
+ });
509
+ });
510
+ };
511
+ element.addEventListener("scroll", onScroll, { passive: true });
512
+ return () => {
513
+ element.removeEventListener("scroll", onScroll);
514
+ if (scrollRafId != null) cancelAnimationFrame(scrollRafId);
515
+ scrollRafPendingRef.current = false;
516
+ frameHadUserScrollRef.current = false;
517
+ };
518
+ }, [chatRoomId, computeRemainingUnread, computeSeparatorVisibility]);
519
+ useEffect(() => {
520
+ computeRemainingUnread();
521
+ computeSeparatorVisibility();
522
+ const raf = requestAnimationFrame(() => {
523
+ computeRemainingUnread();
524
+ computeSeparatorVisibility();
525
+ });
526
+ return () => cancelAnimationFrame(raf);
527
+ }, [chatMessages, computeRemainingUnread, computeSeparatorVisibility]);
528
+ useLayoutEffect(() => {
529
+ const prevRoomId = prevRoomIdRef.current;
530
+ prevRoomIdRef.current = chatRoomId;
531
+ if (prevRoomId && prevRoomId !== chatRoomId) {
532
+ if (useChatStore.getState().isChatAllowed && !checkIsViewAsMode()) {
533
+ markRoomRead(prevRoomId, { isChatAllowed: true }).catch(() => {
534
+ });
535
+ }
536
+ }
537
+ const hasCachedMessages = (useChatStore.getState().ticketChatMessages[chatRoomId]?.length ?? 0) > 0;
538
+ isClearingRef.current = hasCachedMessages;
539
+ if (separatorTimerRef.current) {
540
+ clearTimeout(separatorTimerRef.current);
541
+ separatorTimerRef.current = null;
542
+ }
543
+ roomIdRef.current = null;
544
+ wasAtBottomRef.current = false;
545
+ replyFetchingRef.current = false;
546
+ pinFetchingRef.current = false;
547
+ pendingAnchorRef.current = null;
548
+ loadingOlderRef.current = false;
549
+ bottomMsgIdRef.current = null;
550
+ scrollAnchorRef.current = null;
551
+ prevScrollTopRef.current = 0;
552
+ skipNextPaginationRef.current = false;
553
+ frameHadUserScrollRef.current = false;
554
+ hasInteractedRef.current = false;
555
+ firstNewMsgBoundaryRef.current = null;
556
+ setUnreadConsumed(false);
557
+ setIsScrollingUp(false);
558
+ setIsSeparatorInViewport(false);
559
+ setHasNewMsgWhileAway(false);
560
+ hasAnimatedRef.current = false;
561
+ setIsInitialBatch(false);
562
+ clearTimeout(batchAnimationTimerRef.current);
563
+ shouldAnimateRef.current = !useChatStore.getState().loadedRooms[chatRoomId];
564
+ }, [chatRoomId]);
565
+ useLayoutEffect(() => {
566
+ if (chatMessages.length > 0 && !hasAnimatedRef.current && shouldAnimateRef.current) {
567
+ hasAnimatedRef.current = true;
568
+ setIsInitialBatch(true);
569
+ batchAnimationTimerRef.current = setTimeout(
570
+ () => setIsInitialBatch(false),
571
+ 700
572
+ );
573
+ }
574
+ }, [chatMessages.length, chatRoomId]);
575
+ useLayoutEffect(() => {
576
+ if (!chatMessages.length) return;
577
+ if (roomIdRef.current !== chatRoomId) {
578
+ if (isClearingRef.current) {
579
+ isClearingRef.current = false;
580
+ if (useChatStore.getState().loadedRooms[chatRoomId]) {
581
+ roomIdRef.current = chatRoomId;
582
+ bottomMsgIdRef.current = chatMessages[chatMessages.length - 1]?._id;
583
+ wasAtBottomRef.current = true;
584
+ setIsAtBottom(true);
585
+ skipNextPaginationRef.current = true;
586
+ scrollToBottom("auto");
587
+ return;
588
+ }
589
+ wasAtBottomRef.current = false;
590
+ return;
591
+ }
592
+ roomIdRef.current = chatRoomId;
593
+ bottomMsgIdRef.current = chatMessages[chatMessages.length - 1]?._id;
594
+ wasAtBottomRef.current = true;
595
+ setIsAtBottom(true);
596
+ skipNextPaginationRef.current = true;
597
+ scrollToBottom("auto");
598
+ return;
599
+ }
600
+ const bottomMsg = chatMessages[chatMessages.length - 1];
601
+ if (bottomMsgIdRef.current !== bottomMsg?._id) {
602
+ const prevBottomId = bottomMsgIdRef.current;
603
+ bottomMsgIdRef.current = bottomMsg?._id;
604
+ if (wasAtBottomRef.current || bottomMsg?.chatter_id === user._id) {
605
+ firstNewMsgBoundaryRef.current = null;
606
+ scrollToBottom("auto");
607
+ } else {
608
+ setHasNewMsgWhileAway(true);
609
+ if (!firstNewMsgBoundaryRef.current && prevBottomId) {
610
+ firstNewMsgBoundaryRef.current = prevBottomId;
611
+ }
612
+ const boundaryId = firstNewMsgBoundaryRef.current;
613
+ if (boundaryId) {
614
+ const msgsWithoutSep = chatMessages.filter(
615
+ (m) => !m.isUnreadSeparator
616
+ );
617
+ const boundaryIdx = msgsWithoutSep.findLastIndex(
618
+ (m) => m._id === boundaryId
619
+ );
620
+ if (boundaryIdx !== -1) {
621
+ const newMsgs = msgsWithoutSep.slice(boundaryIdx + 1);
622
+ if (newMsgs.length > 0) {
623
+ const separator = {
624
+ _id: `unread-separator-${chatRoomId}`,
625
+ isUnreadSeparator: true,
626
+ unreadCount: newMsgs.length,
627
+ normalized: true,
628
+ created_at: msgsWithoutSep[boundaryIdx]?.created_at ?? (/* @__PURE__ */ new Date(0)).toISOString()
629
+ };
630
+ setTicketChatMessages(chatRoomId, [
631
+ ...msgsWithoutSep.slice(0, boundaryIdx + 1),
632
+ separator,
633
+ ...newMsgs
634
+ ]);
635
+ setUnreadConsumed(false);
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }, [chatMessages, chatRoomId]);
642
+ useLayoutEffect(() => {
643
+ if (pendingAnchorRef.current == null) return;
644
+ const element = containerRef.current;
645
+ const inner = innerRef.current;
646
+ if (!element || !inner) return;
647
+ const anchor = () => {
648
+ element.scrollTop = element.scrollHeight - prevDistanceFromBottomRef.current;
649
+ };
650
+ anchor();
651
+ const ro = new ResizeObserver(anchor);
652
+ ro.observe(inner);
653
+ const timer = setTimeout(() => {
654
+ ro.disconnect();
655
+ pendingAnchorRef.current = null;
656
+ }, 300);
657
+ return () => {
658
+ clearTimeout(timer);
659
+ ro.disconnect();
660
+ };
661
+ }, [chatMessages]);
662
+ const totalSize = virtualizer.getTotalSize();
663
+ useLayoutEffect(() => {
664
+ const element = containerRef.current;
665
+ if (!element || !chatMessages.length) return;
666
+ if (pendingAnchorRef.current != null) return;
667
+ if (wasAtBottomRef.current) {
668
+ element.scrollTop = element.scrollHeight;
669
+ return;
670
+ }
671
+ const anchor = scrollAnchorRef.current;
672
+ if (!anchor) return;
673
+ const anchoredItem = virtualItemsRef.current.find(
674
+ (item) => item.key === anchor.key
675
+ );
676
+ if (!anchoredItem) return;
677
+ const target = anchoredItem.start + anchor.offset;
678
+ if (Math.abs(element.scrollTop - target) > 1) {
679
+ element.scrollTop = target;
680
+ }
681
+ }, [totalSize]);
682
+ useEffect(() => {
683
+ if (!hasInteractedRef.current) return;
684
+ if (!isAtBottom && remainingUnread > 0) {
685
+ if (separatorTimerRef.current) {
686
+ clearTimeout(separatorTimerRef.current);
687
+ separatorTimerRef.current = null;
688
+ }
689
+ return;
690
+ }
691
+ if (separatorTimerRef.current) return;
692
+ const sepIdx = chatMessages.findIndex((m) => m.isUnreadSeparator);
693
+ if (sepIdx === -1) return;
694
+ const unreadMessages = chatMessages.slice(sepIdx + 1).filter((m) => m._id && !m.isUnreadSeparator);
695
+ if (!unreadMessages.length) return;
696
+ const roomId = chatRoomId;
697
+ const messageIds = unreadMessages.map((m) => m._id);
698
+ separatorTimerRef.current = setTimeout(() => {
699
+ separatorTimerRef.current = null;
700
+ if (useChatStore.getState().isChatAllowed && !checkIsViewAsMode()) {
701
+ markRoomRead(roomId, { messageIds, isChatAllowed: true }).catch(
702
+ () => {
703
+ }
704
+ );
705
+ updateChatUnread(roomId, { unread: 0 });
706
+ }
707
+ }, 1500);
708
+ }, [isAtBottom, remainingUnread, chatRoomId]);
709
+ const isLoading = chatMessages.length === 0 && !isRoomLoaded || isRefetching;
710
+ useEffect(() => {
711
+ clearTimeout(barTimerRef.current);
712
+ if (isLoading) {
713
+ setBarVisible(true);
714
+ setBarProgress(0);
715
+ let p = 0;
716
+ const tick = () => {
717
+ p = Math.min(88, p + Math.max(1.5, (88 - p) * 0.12));
718
+ setBarProgress(p);
719
+ if (p < 88) barTimerRef.current = setTimeout(tick, 50);
720
+ };
721
+ barTimerRef.current = setTimeout(tick, 30);
722
+ } else {
723
+ setBarProgress(100);
724
+ barTimerRef.current = setTimeout(() => setBarVisible(false), 350);
725
+ }
726
+ return () => clearTimeout(barTimerRef.current);
727
+ }, [isLoading]);
728
+ const { handleSendReminderRequest, handleDeleteReminder } = useSendZaloReminder(ticket);
729
+ const handleReminderAction = useCallback(
730
+ ({ data, type = MessageType.UPDATE_REMINDER }) => {
731
+ if (type === MessageType.UPDATE_REMINDER) {
732
+ setIsReminderDialogOpen(true);
733
+ if (data) {
734
+ setReminderData(data);
735
+ }
736
+ } else if (type === MessageType.DELETE_REMINDER) {
737
+ handleDeleteReminder(data);
738
+ }
739
+ },
740
+ [handleDeleteReminder]
741
+ );
742
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative z-[5]", "min-h-0 flex-1", className), children: [
743
+ /* @__PURE__ */ jsxs(
744
+ "div",
745
+ {
746
+ ref: containerRef,
747
+ className: `h-full gap-2 overflow-y-scroll bg-chat-thread-bg px-4 pb-4 ${pinnedData?.data?.data?.length ? "pt-20" : ""}`,
748
+ style: scrollPaddingBottom ? { paddingBottom: scrollPaddingBottom } : void 0,
749
+ children: [
750
+ /* @__PURE__ */ jsx(
751
+ PinnedMessages,
752
+ {
753
+ isChatAllowed,
754
+ chatRoomId,
755
+ ticket,
756
+ pinnedData
757
+ }
758
+ ),
759
+ /* @__PURE__ */ jsx(
760
+ "div",
761
+ {
762
+ ref: innerRef,
763
+ style: {
764
+ height: `${virtualizer.getTotalSize()}px`,
765
+ width: "100%",
766
+ position: "relative"
767
+ },
768
+ children: virtualItems.map((virtualRow, visibleIdx) => {
769
+ const message = chatMessages[virtualRow.index];
770
+ if (!message) return null;
771
+ const staggerDelay = isInitialBatch ? Math.min((virtualItems.length - 1 - visibleIdx) * 25, 300) : 0;
772
+ const animationStyle = isInitialBatch ? {
773
+ animationDelay: `${staggerDelay}ms`,
774
+ animationFillMode: "both"
775
+ } : void 0;
776
+ if (message.isUnreadSeparator) {
777
+ return /* @__PURE__ */ jsx(
778
+ "div",
779
+ {
780
+ "data-index": virtualRow.index,
781
+ ref: virtualizer.measureElement,
782
+ style: {
783
+ position: "absolute",
784
+ top: 0,
785
+ left: 0,
786
+ width: "100%",
787
+ transform: `translateY(${virtualRow.start}px)`
788
+ },
789
+ children: /* @__PURE__ */ jsx(
790
+ "div",
791
+ {
792
+ className: cn(isInitialBatch && "animate-message-cleave"),
793
+ style: animationStyle,
794
+ children: /* @__PURE__ */ jsxs("div", { className: "my-2 flex items-center gap-3", children: [
795
+ /* @__PURE__ */ jsx(
796
+ "div",
797
+ {
798
+ className: cn(
799
+ "h-px flex-1 bg-primary/30",
800
+ separatorHighlighted && "animate-pulse"
801
+ )
802
+ }
803
+ ),
804
+ /* @__PURE__ */ jsx(
805
+ "span",
806
+ {
807
+ className: cn(
808
+ "rounded-full bg-primary/10 px-3 py-0.5 text-xs font-medium text-primary transition-all duration-300",
809
+ separatorHighlighted && "scale-105 bg-primary/25"
810
+ ),
811
+ children: t("chat:label.newMessages", {
812
+ defaultValue: "Tin nh\u1EAFn m\u1EDBi"
813
+ })
814
+ }
815
+ ),
816
+ /* @__PURE__ */ jsx(
817
+ "div",
818
+ {
819
+ className: cn(
820
+ "h-px flex-1 bg-primary/30",
821
+ separatorHighlighted && "animate-pulse"
822
+ )
823
+ }
824
+ )
825
+ ] })
826
+ }
827
+ )
828
+ },
829
+ virtualRow.key
830
+ );
831
+ }
832
+ const nextMessage = virtualRow.index < chatMessages.length - 1 ? chatMessages[virtualRow.index + 1] : null;
833
+ const isSelected = isSelectMode && selectedMessages.some((m) => m._id === message._id);
834
+ const isActionMessage = ActionMessageTypes.includes(
835
+ message.content?.type
836
+ );
837
+ const canSelect = isSelectMode && !isActionMessage;
838
+ return /* @__PURE__ */ jsx(
839
+ "div",
840
+ {
841
+ "data-index": virtualRow.index,
842
+ ref: virtualizer.measureElement,
843
+ style: {
844
+ position: "absolute",
845
+ top: 0,
846
+ left: 0,
847
+ width: "100%",
848
+ transform: `translateY(${virtualRow.start}px)`
849
+ },
850
+ children: /* @__PURE__ */ jsxs(
851
+ "div",
852
+ {
853
+ className: cn(isInitialBatch && "animate-message-cleave"),
854
+ style: animationStyle,
855
+ children: [
856
+ message.dateLabel && /* @__PURE__ */ jsx(SeparatorWithText, { text: message.dateLabel }),
857
+ /* @__PURE__ */ jsx(
858
+ ChatMessage,
859
+ {
860
+ isChatAllowed,
861
+ message,
862
+ ticketSource,
863
+ nextMessage,
864
+ chatRoom: chatRoom ?? ticket,
865
+ ticket,
866
+ reminderAction: handleReminderAction,
867
+ requester,
868
+ pinnedData,
869
+ zaloAllocatedList,
870
+ isSelectMode: canSelect,
871
+ isSelected: canSelect && isSelected,
872
+ onSelect: canSelect ? toggleSelectedMessage : void 0
873
+ }
874
+ ),
875
+ renderMessageExtra?.(message)
876
+ ]
877
+ }
878
+ )
879
+ },
880
+ virtualRow.key
881
+ );
882
+ })
883
+ }
884
+ )
885
+ ]
886
+ }
887
+ ),
888
+ barVisible && /* @__PURE__ */ jsx(
889
+ "div",
890
+ {
891
+ className: cn(
892
+ "absolute inset-x-0 top-0 z-10 h-0.5 bg-primary/20 transition-opacity duration-200",
893
+ barProgress >= 100 && "opacity-0"
894
+ ),
895
+ children: /* @__PURE__ */ jsx(
896
+ "div",
897
+ {
898
+ className: "h-full bg-primary transition-[width] ease-out",
899
+ style: {
900
+ width: `${barProgress}%`,
901
+ transitionDuration: barProgress >= 100 ? "180ms" : "60ms"
902
+ }
903
+ }
904
+ )
905
+ }
906
+ ),
907
+ !unreadConsumed && chatMessages.some((m) => m.isUnreadSeparator) && !isSeparatorInViewport && /* @__PURE__ */ jsx(
908
+ "div",
909
+ {
910
+ className: "absolute right-1/2 top-4 z-50 translate-x-1/2 cursor-pointer duration-200 animate-in fade-in slide-in-from-top-2",
911
+ onClick: () => {
912
+ setUnreadConsumed(true);
913
+ scrollToUnreadOrBottom("auto");
914
+ clearTimeout(separatorHighlightTimerRef.current);
915
+ setSeparatorHighlighted(true);
916
+ separatorHighlightTimerRef.current = setTimeout(
917
+ () => setSeparatorHighlighted(false),
918
+ 1500
919
+ );
920
+ },
921
+ children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary px-3 py-1.5 text-xs font-medium text-white shadow-md ring-2 ring-white", children: [
922
+ /* @__PURE__ */ jsx(ChevronsUp, { size: 14, className: "animate-bounce" }),
923
+ t("chat:label.unreadMessages", { defaultValue: "Tin ch\u01B0a \u0111\u1ECDc" })
924
+ ] })
925
+ }
926
+ ),
927
+ !isAtBottom && /* @__PURE__ */ jsxs(
928
+ "div",
929
+ {
930
+ className: "absolute bottom-4 right-1/2 z-50 flex translate-x-1/2 cursor-pointer flex-col items-center gap-1.5",
931
+ onClick: () => {
932
+ scrollToBottom("auto");
933
+ requestAnimationFrame(() => scrollToBottom("auto"));
934
+ },
935
+ "aria-label": "Scroll to bottom",
936
+ children: [
937
+ hasNewMsgWhileAway && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary px-3 py-1 text-xs font-bold text-white shadow-lg shadow-primary/40 ring-2 ring-white", children: t("chat:label.newMessages", { defaultValue: "Tin nh\u1EAFn m\u1EDBi" }) }),
938
+ /* @__PURE__ */ jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-full bg-card shadow-md ring-1 ring-black/5 transition hover:bg-surface-subtle", children: /* @__PURE__ */ jsx(
939
+ ChevronDown,
940
+ {
941
+ size: 18,
942
+ className: "text-muted-foreground",
943
+ strokeWidth: 2.5
944
+ }
945
+ ) })
946
+ ]
947
+ }
948
+ ),
949
+ isReminderDialogOpen && /* @__PURE__ */ jsx(
950
+ CreateZaloReminder,
951
+ {
952
+ ticket,
953
+ requester,
954
+ reminderData,
955
+ open: isReminderDialogOpen,
956
+ onOpenChange: setIsReminderDialogOpen,
957
+ onSubmit: handleSendReminderRequest
958
+ }
959
+ ),
960
+ feedbackMessage && (renderFeedbackDialog ? renderFeedbackDialog({
961
+ open: !!feedbackMessage,
962
+ onOpenChange: (open) => setFeedbackMessage(open ? feedbackMessage : null),
963
+ message: feedbackMessage
964
+ }) : /* @__PURE__ */ jsx(
965
+ ChatFeedback,
966
+ {
967
+ open: !!feedbackMessage,
968
+ onOpenChange: (open) => setFeedbackMessage(open ? feedbackMessage : null),
969
+ message: feedbackMessage
970
+ }
971
+ )),
972
+ insightMessage && /* @__PURE__ */ jsx(
973
+ ChatInsight,
974
+ {
975
+ open: !!insightMessage,
976
+ onOpenChange: (open) => setInsightMessage(open ? insightMessage : null),
977
+ message: insightMessage
978
+ }
979
+ ),
980
+ /* @__PURE__ */ jsx(
981
+ ForwardMessageModal,
982
+ {
983
+ open: !!forwardMessage,
984
+ data: forwardMessage,
985
+ onClose: () => {
986
+ setForwardMessage(null);
987
+ setSelectMode(false);
988
+ }
989
+ }
990
+ )
991
+ ] });
992
+ }
993
+ export {
994
+ ChatMessageList as default
995
+ };