@tuturuuu/ui 0.25.2 → 0.26.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 (36) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/biome.json +1 -1
  3. package/package.json +13 -12
  4. package/src/components/ui/chat/chat-sidebar-items.tsx +25 -19
  5. package/src/components/ui/chat/chat-utils.test.ts +88 -2
  6. package/src/components/ui/chat/chat-workspace.tsx +1 -0
  7. package/src/components/ui/chat/composer-attachment-chip.tsx +128 -0
  8. package/src/components/ui/chat/external-message-content.test.ts +41 -0
  9. package/src/components/ui/chat/external-message-content.ts +28 -0
  10. package/src/components/ui/chat/message-bubble.tsx +4 -2
  11. package/src/components/ui/chat/message-composer.tsx +121 -33
  12. package/src/components/ui/chat/message-links.test.tsx +14 -0
  13. package/src/components/ui/chat/utils.ts +39 -3
  14. package/src/components/ui/custom/education/courses/course-row-actions.tsx +1 -1
  15. package/src/components/ui/custom/education/modules/course-module-row-actions.tsx +1 -1
  16. package/src/components/ui/custom/structure.test.tsx +55 -1
  17. package/src/components/ui/custom/structure.tsx +10 -1
  18. package/src/components/ui/custom/tables/custom-data-table.tsx +3 -2
  19. package/src/components/ui/custom/tables/data-table-column-header.tsx +4 -3
  20. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +4 -3
  21. package/src/components/ui/custom/tables/data-table-pagination.tsx +6 -6
  22. package/src/components/ui/custom/tables/data-table-toolbar.tsx +5 -5
  23. package/src/components/ui/custom/tables/data-table-view-options.tsx +4 -3
  24. package/src/components/ui/custom/tables/data-table.tsx +69 -30
  25. package/src/components/ui/finance/invoices/columns.test.tsx +3 -3
  26. package/src/components/ui/finance/invoices/columns.tsx +4 -2
  27. package/src/components/ui/finance/invoices/pending-columns.tsx +1 -1
  28. package/src/components/ui/finance/invoices/row-actions.tsx +1 -1
  29. package/src/components/ui/finance/transactions/categories/columns.test.tsx +5 -5
  30. package/src/components/ui/finance/transactions/categories/columns.tsx +4 -2
  31. package/src/components/ui/finance/transactions/categories/row-actions.tsx +1 -1
  32. package/src/components/ui/finance/transactions/columns.test.tsx +5 -5
  33. package/src/components/ui/finance/transactions/columns.tsx +4 -2
  34. package/src/components/ui/finance/transactions/row-actions.tsx +1 -1
  35. package/src/components/ui/finance/wallets/columns.tsx +4 -2
  36. package/src/components/ui/finance/wallets/row-actions.tsx +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,117 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.26.0](https://github.com/tutur3u/platform/compare/ui-v0.25.3...ui-v0.26.0) (2026-08-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **apps:** add resilient error recovery shells ([f0f514d](https://github.com/tutur3u/platform/commit/f0f514d2b1712ea76c6845801fa2803369418a63))
9
+ * **chat:** add connected site sync bridge ([05c27f5](https://github.com/tutur3u/platform/commit/05c27f5ac4a545af366097ee2a919ec755970b9b))
10
+ * **chat:** add connected site sync bridge ([#5078](https://github.com/tutur3u/platform/issues/5078)) ([3de1a07](https://github.com/tutur3u/platform/commit/3de1a07dfa2fec96982aa0d116e003d0aa3fe47c))
11
+ * **chat:** add external parity reconciliation ([953b9d6](https://github.com/tutur3u/platform/commit/953b9d6315611ef86ec2c213bbbbc5e62d5f4ad4))
12
+ * **chat:** add external parity reconciliation ([#5086](https://github.com/tutur3u/platform/issues/5086)) ([5ef796f](https://github.com/tutur3u/platform/commit/5ef796f7812ec6a9f9a62193ba21633cd2503001))
13
+ * **chat:** complete connected-site operational parity ([#5093](https://github.com/tutur3u/platform/issues/5093)) ([397e11b](https://github.com/tutur3u/platform/commit/397e11bd87d583fe1c65f83a2b8019c287650e19))
14
+ * **chat:** complete connected-site parity ([fd4061d](https://github.com/tutur3u/platform/commit/fd4061d8b2f654e521c40ea9819a348ae81575c9))
15
+ * **finance:** reconcile inventory provider sales ([56650e0](https://github.com/tutur3u/platform/commit/56650e066c473337700998d8af607380e8ea6202))
16
+ * **inventory:** allocate bundle revenue and add cash checkout ([bd465f9](https://github.com/tutur3u/platform/commit/bd465f9bb35fad16d259378fa49611a8eb5ba95b))
17
+ * **inventory:** secure Square POS event checkout ([532b463](https://github.com/tutur3u/platform/commit/532b46372116ee0bebfd83ba2af762cc9f668c3c))
18
+ * **landing:** retile the app bento and rebuild the problem section ([c56f42a](https://github.com/tutur3u/platform/commit/c56f42adf754362a269ff08b380db1ee0cf8c6ca))
19
+ * **meet:** revamp collaborative scheduling ([9124a5d](https://github.com/tutur3u/platform/commit/9124a5d559e071c7e6c1c713cfbca4d9f5205611))
20
+ * **navigation:** open workspace selector on reveal ([c760d42](https://github.com/tutur3u/platform/commit/c760d42b5e0b8f7bd141e26e2ace956b7f39912e))
21
+ * **offline:** own service worker runtime and refresh dependencies ([ae44477](https://github.com/tutur3u/platform/commit/ae44477603c39f0513244514771653287338a89f))
22
+ * **onboarding:** connect product guidance across apps ([68cf626](https://github.com/tutur3u/platform/commit/68cf626c9650e5044b6c123f9423a6cebf1bba9a))
23
+ * **reports:** add periodic reporting automation ([ec7bd5e](https://github.com/tutur3u/platform/commit/ec7bd5e10abb137e217d1dcf143624530276392f))
24
+ * **ui:** align landing and locale experience ([aa6e47f](https://github.com/tutur3u/platform/commit/aa6e47f17356ce74111ccf130e8b17071cc7aadf))
25
+ * **web:** improve workspace navigation context ([73959e3](https://github.com/tutur3u/platform/commit/73959e3c6745f60513d23740f30024c5d24e7ee4))
26
+ * **web:** redesign marketing landing page ([13ede78](https://github.com/tutur3u/platform/commit/13ede784ac03af9f25bbb7df61095826c335e002))
27
+ * **web:** refine marketing experience ([c7f1cec](https://github.com/tutur3u/platform/commit/c7f1cec0dd667e6d5f59aaf0bab82069b79c7376))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **auth:** isolate invitation caches by user ([c720f15](https://github.com/tutur3u/platform/commit/c720f1551b1276bedf393bd4fc8975e33f64aa83))
33
+ * **auth:** resume a pending sign-in after switching accounts ([5403718](https://github.com/tutur3u/platform/commit/54037189ee895863eac621239eb54b8ea94c6ed0))
34
+ * **chat:** address external bridge review findings ([30533fc](https://github.com/tutur3u/platform/commit/30533fc64093fed4b25279bc0e45d43bdb7a6cde))
35
+ * **chat:** close bridge edge cases ([db585c8](https://github.com/tutur3u/platform/commit/db585c8486064837dcbce615a30f66c38c0a3ec0))
36
+ * **chat:** decode external visitor labels ([1ca3e75](https://github.com/tutur3u/platform/commit/1ca3e75a939c06222c842da9fe5892a1bf9ce729))
37
+ * **chat:** decode external visitor labels ([#5112](https://github.com/tutur3u/platform/issues/5112)) ([cb7af54](https://github.com/tutur3u/platform/commit/cb7af54264344961234da681c877c5677bf950fa))
38
+ * **chat:** harden bridge review edge cases ([0ecfad7](https://github.com/tutur3u/platform/commit/0ecfad7c66454fdb3bcd971f1362d3789287f086))
39
+ * **chat:** harden connected site migration ([73cccd0](https://github.com/tutur3u/platform/commit/73cccd04e9bd85a443891f3e52b443ef755570cb))
40
+ * **chat:** harden connected site sync ([70e8785](https://github.com/tutur3u/platform/commit/70e8785967e145a77330272e671ca58dfc38fbee))
41
+ * **chat:** harden connected-site parity ([a0d3148](https://github.com/tutur3u/platform/commit/a0d31483a87ed0ebd59f2be5c3bdbb342bd67855))
42
+ * **chat:** harden external bridge boundaries ([2d25202](https://github.com/tutur3u/platform/commit/2d252023e5cfb1c7414ee2ed2d161e7f5de613f8))
43
+ * **chat:** harden external bridge synchronization ([5b7c709](https://github.com/tutur3u/platform/commit/5b7c7092260b05c8de86c2383291433f81c9eead))
44
+ * **chat:** harden external bridge synchronization ([63ccf91](https://github.com/tutur3u/platform/commit/63ccf9129ff8e7cf255bef8c74a3e096ab45cb7d))
45
+ * **chat:** harden external parity replay ([896923b](https://github.com/tutur3u/platform/commit/896923b3c498a09fe0974de1aa0f8d291f8b2c84))
46
+ * **chat:** harden external parity replay ([89c5498](https://github.com/tutur3u/platform/commit/89c5498aef4ad5ebf05ef93bfb05168417a5dfe2))
47
+ * **chat:** mount query state adapter ([#5084](https://github.com/tutur3u/platform/issues/5084)) ([60253f8](https://github.com/tutur3u/platform/commit/60253f82477fc54923556706d12e4dc525a7a170))
48
+ * **chat:** preserve native message behavior ([fc09530](https://github.com/tutur3u/platform/commit/fc095301f2d26ca2bc39de02ce17b1f341327de9))
49
+ * **chat:** restore external inbox display parity ([#5108](https://github.com/tutur3u/platform/issues/5108)) ([b34ea6d](https://github.com/tutur3u/platform/commit/b34ea6d7d8ffda344053ed1f580128446d0d0a81))
50
+ * **ci:** keep task helper collection source-only ([7355a05](https://github.com/tutur3u/platform/commit/7355a05385ccd8d1e2d1bcf6ac15ae5297e057ad))
51
+ * **ci:** resolve shared task helper during e2e collection ([0319a3b](https://github.com/tutur3u/platform/commit/0319a3bd0df12592b823b7cebdff38cea3e50836))
52
+ * **deps:** align Streamdown with Shiki 4 ([0d89f55](https://github.com/tutur3u/platform/commit/0d89f555b3228307e893a5d7723aaf8db8505d63))
53
+ * **inventory:** correct storefront bundle checkout ([cb9c247](https://github.com/tutur3u/platform/commit/cb9c24740ceb77f9426ba640383a68bd9ed95d2c))
54
+ * **platform:** improve satellite workspace routing ([0c49c48](https://github.com/tutur3u/platform/commit/0c49c4882d26704fb16ba94ee289ab0af7deb4de))
55
+ * **platform:** merge notification and group visibility fixes ([4fe9e97](https://github.com/tutur3u/platform/commit/4fe9e970bf61bffaee4353b1ebb83ce2f880a4c8))
56
+ * **platform:** persist settings dialog state ([04d2128](https://github.com/tutur3u/platform/commit/04d212807b6fabf33f43743c0b779ba9499334ba))
57
+ * **platform:** restore notifications and group visibility ([eb570a4](https://github.com/tutur3u/platform/commit/eb570a47e7a3d38fc855fbf3e887ecbde853ece0))
58
+ * **platform:** stabilize offline and calendar initialization ([8ef8fbc](https://github.com/tutur3u/platform/commit/8ef8fbc1274e108930adf38bb8e998384551058b))
59
+ * resolve code quality findings ([63f10b5](https://github.com/tutur3u/platform/commit/63f10b5ec22a4194f48f448ee2b1b088b5da8f08))
60
+ * **security:** remediate code scanning findings ([023db2e](https://github.com/tutur3u/platform/commit/023db2edf4b0557be108a9d772cbc7e2223af947))
61
+ * **settings:** harden workspace role management ([0570687](https://github.com/tutur3u/platform/commit/0570687d255698e438d1eb02262ec0fa7c56240f))
62
+ * **settings:** repair satellite workspace management ([63614cd](https://github.com/tutur3u/platform/commit/63614cdd1550cbf7084724dbed728e798b6f979c))
63
+ * **tasks:** quiet the description toolbar and name the compact action honestly ([51838a0](https://github.com/tutur3u/platform/commit/51838a08a1875a44cd4c7f539b35203f890e1a39))
64
+ * **tasks:** repair board share access and harden AI media attachments ([65b8092](https://github.com/tutur3u/platform/commit/65b809245cc01fb7a5f034f703083f329c20f1c1))
65
+ * **tasks:** repair onboarding and external metadata ([e0b62eb](https://github.com/tutur3u/platform/commit/e0b62eb7119155f6e4cad3dc4fb4d0f9820c98e8))
66
+ * **tasks:** restore task description on deep-link opens ([56d4efa](https://github.com/tutur3u/platform/commit/56d4efabd2f036e7dae39588d8aed7b129fa9a11))
67
+ * **tasks:** show one skeleton per task dialog open ([944288b](https://github.com/tutur3u/platform/commit/944288bdc869ee80b3cdf537ee10feb17fdada22))
68
+ * **tasks:** stop the task dialog remounting after it opens ([9a423da](https://github.com/tutur3u/platform/commit/9a423daf7e7c1e901a63ddcc1a8a94ddff9192c9))
69
+ * **ui:** make member management mobile responsive ([104c812](https://github.com/tutur3u/platform/commit/104c8120949ef34bfdc48da74fdc0e2d35eb4ce7))
70
+ * **ui:** preserve hosted workspace logos ([1f8e8b2](https://github.com/tutur3u/platform/commit/1f8e8b28c23f659952ad58f00a2a371acd8fcef9))
71
+ * **ui:** publish portable vendored dependencies ([99ad359](https://github.com/tutur3u/platform/commit/99ad35903a27b8f9df26f22fc5f1003aed363d6f))
72
+ * **ui:** refine member access settings ([b5b1ebf](https://github.com/tutur3u/platform/commit/b5b1ebf49a4a3b93532da74357899927f6e5d988))
73
+ * **ui:** unify workspace access and sidebar controls ([450183e](https://github.com/tutur3u/platform/commit/450183ef78af8e09a386bebb93be7018379c5152))
74
+ * **web:** harden prerender client boundaries ([ab1596a](https://github.com/tutur3u/platform/commit/ab1596a769e5a6ae43b0247c651ecbecea52d20d))
75
+ * **workspaces:** harden invitation interactions ([b7f8f6c](https://github.com/tutur3u/platform/commit/b7f8f6cf52ceec1b67d36b11513eae7806284f5d))
76
+ * **workspaces:** propagate a workspace rename without a page reload ([cca0b2d](https://github.com/tutur3u/platform/commit/cca0b2d7e14f6d72f1fdda1deaac18aaf07e9eee))
77
+ * **workspaces:** repair shared member administration ([8a1515b](https://github.com/tutur3u/platform/commit/8a1515ba970adbe690b9d92f77a447adb93c339f))
78
+ * **workspaces:** restore invitation access across apps ([3592699](https://github.com/tutur3u/platform/commit/35926990fc28e44996fd330c6403496d78d96872))
79
+ * **workspaces:** restore invitation access across apps ([#5099](https://github.com/tutur3u/platform/issues/5099)) ([c7032c3](https://github.com/tutur3u/platform/commit/c7032c310639c2783b60ac560e83a84d65a5c7f5))
80
+ * **workspaces:** route satellite creation through setup ([754bf1b](https://github.com/tutur3u/platform/commit/754bf1b81360e4755a171819f3a8e6a7c102f351))
81
+
82
+
83
+ ### Performance Improvements
84
+
85
+ * **inventory:** add event-driven storefront caching ([2f571f4](https://github.com/tutur3u/platform/commit/2f571f4fc534bf5dee0ca36c4310d87b44e6e613))
86
+ * **storefront:** share customer navigation shell ([21cb78d](https://github.com/tutur3u/platform/commit/21cb78db484d1e1fdb9f2469fcac4e3a948bd176))
87
+ * **storefront:** stabilize navigation and cart rendering ([471c6b7](https://github.com/tutur3u/platform/commit/471c6b75abc6a4d69692e868cc1b220785e03262))
88
+
89
+ ## [0.25.3](https://github.com/tutur3u/platform/compare/ui-v0.25.2...ui-v0.25.3) (2026-08-09)
90
+
91
+
92
+ ### Bug Fixes
93
+
94
+ * **chat:** decode external visitor labels ([1ca3e75](https://github.com/tutur3u/platform/commit/1ca3e75a939c06222c842da9fe5892a1bf9ce729))
95
+ * **chat:** decode external visitor labels ([#5112](https://github.com/tutur3u/platform/issues/5112)) ([cb7af54](https://github.com/tutur3u/platform/commit/cb7af54264344961234da681c877c5677bf950fa))
96
+ * **platform:** merge notification and group visibility fixes ([4fe9e97](https://github.com/tutur3u/platform/commit/4fe9e970bf61bffaee4353b1ebb83ce2f880a4c8))
97
+ * **platform:** restore notifications and group visibility ([eb570a4](https://github.com/tutur3u/platform/commit/eb570a47e7a3d38fc855fbf3e887ecbde853ece0))
98
+
99
+ ## [0.25.2](https://github.com/tutur3u/platform/compare/ui-v0.25.1...ui-v0.25.2) (2026-08-07)
100
+
101
+
102
+ ### Bug Fixes
103
+
104
+ * **chat:** restore external inbox display parity ([#5108](https://github.com/tutur3u/platform/issues/5108)) ([b34ea6d](https://github.com/tutur3u/platform/commit/b34ea6d7d8ffda344053ed1f580128446d0d0a81))
105
+ * **tasks:** repair board share access and harden AI media attachments ([65b8092](https://github.com/tutur3u/platform/commit/65b809245cc01fb7a5f034f703083f329c20f1c1))
106
+
107
+ ## [0.25.1](https://github.com/tutur3u/platform/compare/ui-v0.25.0...ui-v0.25.1) (2026-08-06)
108
+
109
+
110
+ ### Bug Fixes
111
+
112
+ * **workspaces:** harden invitation interactions ([b7f8f6c](https://github.com/tutur3u/platform/commit/b7f8f6cf52ceec1b67d36b11513eae7806284f5d))
113
+ * **workspaces:** restore invitation access across apps ([#5099](https://github.com/tutur3u/platform/issues/5099)) ([c7032c3](https://github.com/tutur3u/platform/commit/c7032c310639c2783b60ac560e83a84d65a5c7f5))
114
+
3
115
  ## [0.25.0](https://github.com/tutur3u/platform/compare/ui-v0.24.0...ui-v0.25.0) (2026-08-06)
4
116
 
5
117
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,7 +58,7 @@
58
58
  "@tanstack/react-hotkeys": "^0.10.0",
59
59
  "@tanstack/react-pacer": "^0.22.1",
60
60
  "@tanstack/react-query": "^5.101.2",
61
- "@tanstack/react-table": "^8.21.3",
61
+ "@tanstack/react-table": "^9.0.0",
62
62
  "@tanstack/react-virtual": "^3.14.9",
63
63
  "@tiptap/core": "3.29.2",
64
64
  "@tiptap/extension-collaboration": "3.29.2",
@@ -86,13 +86,13 @@
86
86
  "@tiptap/pm": "3.29.2",
87
87
  "@tiptap/react": "3.29.2",
88
88
  "@tiptap/starter-kit": "3.29.2",
89
- "@tuturuuu/ai": "0.8.0",
89
+ "@tuturuuu/ai": "0.8.1",
90
90
  "@tuturuuu/apis": "0.11.0",
91
91
  "@tuturuuu/hooks": "0.1.0",
92
92
  "@tuturuuu/icons": "0.1.0",
93
- "@tuturuuu/internal-api": "0.28.0",
93
+ "@tuturuuu/internal-api": "0.29.0",
94
94
  "@tuturuuu/supabase": "0.5.0",
95
- "@tuturuuu/utils": "0.22.0",
95
+ "@tuturuuu/utils": "0.24.0",
96
96
  "@types/debug": "^4.1.13",
97
97
  "browser-image-compression": "^2.0.2",
98
98
  "class-variance-authority": "^0.7.1",
@@ -104,18 +104,19 @@
104
104
  "debug": "^4.4.3",
105
105
  "embla-carousel-react": "^8.6.0",
106
106
  "eventemitter3": "^5.0.4",
107
- "framer-motion": "^12.43.0",
107
+ "framer-motion": "^13.0.0",
108
108
  "gsap": "^3.15.0",
109
+ "html-entities": "^2.6.0",
109
110
  "html2canvas-pro": "^2.3.3",
110
111
  "input-otp": "^1.4.2",
111
112
  "katex": "^0.18.1",
112
113
  "lodash": "4.18.1",
113
114
  "moment": "^2.30.1",
114
- "motion": "^12.43.0",
115
+ "motion": "^13.0.0",
115
116
  "next": "^16.3.0",
116
- "next-intl": "^4.13.4",
117
+ "next-intl": "^4.13.5",
117
118
  "next-themes": "^0.4.6",
118
- "nuqs": "^2.9.3",
119
+ "nuqs": "^2.9.5",
119
120
  "prosemirror-state": "^1.4.4",
120
121
  "prosemirror-view": "1.42.2",
121
122
  "qrcode.react": "^4.2.0",
@@ -142,14 +143,14 @@
142
143
  "use-debounce": "^10.1.1",
143
144
  "vaul": "^1.1.2",
144
145
  "y-protocols": "^1.0.7",
145
- "yjs": "^13.6.31",
146
+ "yjs": "^13.6.32",
146
147
  "zod": "^4.4.3"
147
148
  },
148
149
  "devDependencies": {
149
150
  "@tailwindcss/postcss": "^4.3.3",
150
151
  "@tailwindcss/typography": "^0.5.20",
151
152
  "@tanstack/react-query": "^5.101.4",
152
- "@tanstack/react-table": "^8.21.3",
153
+ "@tanstack/react-table": "^9.0.0",
153
154
  "@testing-library/jest-dom": "^7.0.0",
154
155
  "@testing-library/react": "^16.3.2",
155
156
  "@tuturuuu/types": "0.27.0",
@@ -174,7 +175,7 @@
174
175
  },
175
176
  "peerDependencies": {
176
177
  "@tanstack/react-query": "^5.101.2",
177
- "@tanstack/react-table": "^8.21.3"
178
+ "@tanstack/react-table": "^9.0.0"
178
179
  },
179
180
  "exports": {
180
181
  "./globals.css": "./src/globals.css",
@@ -6,6 +6,7 @@ import { cn } from '@tuturuuu/utils/format';
6
6
  import { useLocale, useTranslations } from 'next-intl';
7
7
  import { Button } from '../button';
8
8
  import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
9
+ import { getChatMessageDisplayContent } from './external-message-content';
9
10
  import {
10
11
  getChatConversationQueueDetails,
11
12
  getChatMessageSenderLabel,
@@ -37,6 +38,7 @@ export function ConversationRow({
37
38
  channel: t('untitled_channel'),
38
39
  chat: t('untitled_chat'),
39
40
  direct: t('direct_message'),
41
+ external: t('external_sender'),
40
42
  group: t('group_chat'),
41
43
  });
42
44
  const pinned = isChatConversationPinned(conversation, currentUserId);
@@ -160,25 +162,29 @@ export function SearchResultList({
160
162
 
161
163
  return (
162
164
  <div className="h-full space-y-1 overflow-y-auto p-2">
163
- {messages.map((message) => (
164
- <button
165
- className="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-accent"
166
- key={message.id}
167
- onClick={() => onSelectConversation(message.conversationId)}
168
- type="button"
169
- >
170
- <span className="block truncate font-medium text-sm">
171
- {getChatMessageSenderLabel(message, {
172
- assistant: t('assistant_name'),
173
- external: t('external_sender'),
174
- unknown: t('unknown_sender'),
175
- })}
176
- </span>
177
- <span className="mt-1 line-clamp-2 text-muted-foreground text-xs">
178
- {message.content}
179
- </span>
180
- </button>
181
- ))}
165
+ {messages.map((message) => {
166
+ const displayContent = getChatMessageDisplayContent(message);
167
+
168
+ return (
169
+ <button
170
+ className="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-accent"
171
+ key={message.id}
172
+ onClick={() => onSelectConversation(message.conversationId)}
173
+ type="button"
174
+ >
175
+ <span className="block truncate font-medium text-sm">
176
+ {getChatMessageSenderLabel(message, {
177
+ assistant: t('assistant_name'),
178
+ external: t('external_sender'),
179
+ unknown: t('unknown_sender'),
180
+ })}
181
+ </span>
182
+ <span className="mt-1 line-clamp-2 text-muted-foreground text-xs">
183
+ {displayContent}
184
+ </span>
185
+ </button>
186
+ );
187
+ })}
182
188
  </div>
183
189
  );
184
190
  }
@@ -134,6 +134,62 @@ describe('chat utils', () => {
134
134
  expect(title).toBe('Ada Lovelace');
135
135
  });
136
136
 
137
+ it('gives unnamed external conversations a stable pseudonymous title', () => {
138
+ const title = getConversationTitle(
139
+ conversation({
140
+ id: 'a7d12840-f72d-48f2-8057-a1ea15aecd29',
141
+ metadata: { externalChat: true },
142
+ title: 'External visitor',
143
+ type: 'channel',
144
+ }),
145
+ 'user-1',
146
+ { external: 'Website visitor' }
147
+ );
148
+
149
+ expect(title).toBe('Website visitor #AECD29');
150
+ });
151
+
152
+ it('prefers an external profile name over a persisted generic title', () => {
153
+ const title = getConversationTitle(
154
+ conversation({
155
+ metadata: { displayName: 'Visitor 42', externalChat: true },
156
+ title: 'External visitor',
157
+ type: 'channel',
158
+ }),
159
+ 'user-1',
160
+ { external: 'Website visitor' }
161
+ );
162
+
163
+ expect(title).toBe('Visitor 42');
164
+ });
165
+
166
+ it('decodes external profile and persisted conversation titles', () => {
167
+ expect(
168
+ getConversationTitle(
169
+ conversation({
170
+ metadata: {
171
+ displayName: 'Nguyễn Thị Vi&amp;ecirc;n',
172
+ externalChat: true,
173
+ },
174
+ title: 'External visitor',
175
+ type: 'channel',
176
+ }),
177
+ 'user-1'
178
+ )
179
+ ).toBe('Nguyễn Thị Viên');
180
+
181
+ expect(
182
+ getConversationTitle(
183
+ conversation({
184
+ metadata: { externalChat: true },
185
+ title: 'Nguyễn Thị Vi&ecirc;n',
186
+ type: 'channel',
187
+ }),
188
+ 'user-1'
189
+ )
190
+ ).toBe('Nguyễn Thị Viên');
191
+ });
192
+
137
193
  it('builds useful message previews for files and deleted messages', () => {
138
194
  expect(
139
195
  getLastMessagePreview(
@@ -171,6 +227,21 @@ describe('chat utils', () => {
171
227
  ).toBe('message_deleted');
172
228
  });
173
229
 
230
+ it('decodes external message previews without changing native previews', () => {
231
+ const encoded = 'T&ocirc;i &#273;&atilde; g&#7917;i tin';
232
+
233
+ expect(
234
+ getLastMessagePreview({
235
+ ...baseMessage,
236
+ content: encoded,
237
+ metadata: { externalChat: true },
238
+ })
239
+ ).toBe('Tôi đã gửi tin');
240
+ expect(getLastMessagePreview({ ...baseMessage, content: encoded })).toBe(
241
+ encoded
242
+ );
243
+ });
244
+
174
245
  it('formats file sizes for attachment rows', () => {
175
246
  expect(formatFileSize(0)).toBe('0 B');
176
247
  expect(formatFileSize(2048)).toBe('2.0 KB');
@@ -344,12 +415,12 @@ describe('chat utils', () => {
344
415
  ...baseMessage,
345
416
  metadata: {
346
417
  externalChat: true,
347
- externalSender: { displayName: 'Visitor 42' },
418
+ externalSender: { displayName: 'Visitor &amp;amp; 42' },
348
419
  },
349
420
  },
350
421
  fallback
351
422
  )
352
- ).toBe('Visitor 42');
423
+ ).toBe('Visitor & 42');
353
424
  expect(
354
425
  getChatMessageSenderLabel(
355
426
  { ...baseMessage, metadata: { externalChat: true } },
@@ -382,6 +453,21 @@ describe('chat utils', () => {
382
453
  });
383
454
  });
384
455
 
456
+ it('decodes connected-site queue previews using conversation scope', () => {
457
+ const details = getChatConversationQueueDetails(
458
+ conversation({
459
+ latestMessage: {
460
+ ...baseMessage,
461
+ content: 'T&ocirc;i &#273;&atilde; g&#7917;i tin',
462
+ metadata: { status: 'seen' },
463
+ },
464
+ metadata: { externalChat: true },
465
+ })
466
+ );
467
+
468
+ expect(details.preview).toBe('Tôi đã gửi tin');
469
+ });
470
+
385
471
  it('normalizes connected-site delivery and deletion states', () => {
386
472
  expect(
387
473
  getChatConversationDeliveryState({
@@ -325,6 +325,7 @@ export function ChatWorkspace({
325
325
  channel: t('untitled_channel'),
326
326
  chat: t('untitled_chat'),
327
327
  direct: t('direct_message'),
328
+ external: t('external_sender'),
328
329
  group: t('group_chat'),
329
330
  })
330
331
  : t('title');
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import { FileText, Film, Music, X } from '@tuturuuu/icons';
4
+ import type { ChatAttachmentDraft } from '@tuturuuu/internal-api';
5
+ import { cn } from '@tuturuuu/utils/format';
6
+ import { Button } from '../button';
7
+ import { formatFileSize } from './utils';
8
+
9
+ export type ComposerAttachmentKind = 'audio' | 'file' | 'image' | 'video';
10
+
11
+ export function getComposerAttachmentKind(
12
+ filename: string,
13
+ contentType?: string | null
14
+ ): ComposerAttachmentKind {
15
+ const type = contentType?.toLowerCase() ?? '';
16
+ const extension = filename.split('.').pop()?.toLowerCase() ?? '';
17
+
18
+ if (
19
+ type.startsWith('image/') ||
20
+ ['avif', 'gif', 'heic', 'jpeg', 'jpg', 'png', 'webp'].includes(extension)
21
+ ) {
22
+ return 'image';
23
+ }
24
+ if (
25
+ type.startsWith('video/') ||
26
+ ['m4v', 'mov', 'mp4', 'webm'].includes(extension)
27
+ ) {
28
+ return 'video';
29
+ }
30
+ if (
31
+ type.startsWith('audio/') ||
32
+ ['aac', 'flac', 'm4a', 'mp3', 'ogg', 'opus', 'wav'].includes(extension)
33
+ ) {
34
+ return 'audio';
35
+ }
36
+ return 'file';
37
+ }
38
+
39
+ const kindIcon = {
40
+ audio: Music,
41
+ file: FileText,
42
+ image: FileText,
43
+ video: Film,
44
+ } as const;
45
+
46
+ /**
47
+ * A queued attachment, shown before the message is sent. Media gets a real
48
+ * thumbnail from the local file rather than a filename chip — a picture is the
49
+ * only reliable way to confirm you attached the right screenshot.
50
+ */
51
+ export function ComposerAttachmentChip({
52
+ attachment,
53
+ onRemove,
54
+ previewUrl,
55
+ removeLabel,
56
+ }: {
57
+ attachment: ChatAttachmentDraft;
58
+ onRemove: () => void;
59
+ previewUrl?: string;
60
+ removeLabel: string;
61
+ }) {
62
+ const kind = getComposerAttachmentKind(
63
+ attachment.filename,
64
+ attachment.contentType
65
+ );
66
+ const showsThumbnail = Boolean(previewUrl) && kind !== 'file';
67
+ const Icon = kindIcon[kind];
68
+
69
+ return (
70
+ <div
71
+ className={cn(
72
+ 'group relative flex min-w-0 items-center gap-2 overflow-hidden rounded-md border bg-muted/40 text-sm',
73
+ showsThumbnail ? 'p-1 pr-1' : 'px-2 py-1'
74
+ )}
75
+ >
76
+ {showsThumbnail ? (
77
+ <span className="size-10 shrink-0 overflow-hidden rounded bg-background">
78
+ {kind === 'video' ? (
79
+ <video
80
+ className="size-full object-cover"
81
+ muted
82
+ preload="metadata"
83
+ src={previewUrl}
84
+ />
85
+ ) : kind === 'audio' ? (
86
+ <span className="flex size-full items-center justify-center">
87
+ <Music className="size-4 text-muted-foreground" />
88
+ </span>
89
+ ) : (
90
+ /* biome-ignore lint/performance/noImgElement: local blob: URL for a not-yet-sent file; next/image cannot optimize object URLs */
91
+ <img
92
+ alt={attachment.filename}
93
+ className="size-full object-cover"
94
+ src={previewUrl}
95
+ />
96
+ )}
97
+ </span>
98
+ ) : (
99
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
100
+ )}
101
+
102
+ <span className="min-w-0 flex-1">
103
+ <span
104
+ className="block max-w-40 truncate font-medium"
105
+ title={attachment.filename}
106
+ >
107
+ {attachment.filename}
108
+ </span>
109
+ {attachment.sizeBytes ? (
110
+ <span className="block text-muted-foreground text-xs">
111
+ {formatFileSize(attachment.sizeBytes)}
112
+ </span>
113
+ ) : null}
114
+ </span>
115
+
116
+ <Button
117
+ aria-label={removeLabel}
118
+ className="size-6 shrink-0"
119
+ onClick={onRemove}
120
+ size="icon"
121
+ type="button"
122
+ variant="ghost"
123
+ >
124
+ <X className="size-3.5" />
125
+ </Button>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getChatMessageDisplayContent } from './external-message-content';
3
+
4
+ describe('getChatMessageDisplayContent', () => {
5
+ it('decodes named and numeric entities for external chat messages', () => {
6
+ expect(
7
+ getChatMessageDisplayContent({
8
+ content:
9
+ 'T&ocirc;i &#273;&atilde; g&#7917;i tin &amp; ch&#7901; ph&#7843;n h&#7891;i.',
10
+ metadata: { externalChat: true },
11
+ })
12
+ ).toBe('Tôi đã gửi tin & chờ phản hồi.');
13
+ });
14
+
15
+ it('decodes bounded nested entities from double-encoded legacy rows', () => {
16
+ expect(
17
+ getChatMessageDisplayContent({
18
+ content: 'Kh&aacute;m trực tiếp &amp;amp; mang theo kết quả',
19
+ metadata: { externalChat: true },
20
+ })
21
+ ).toBe('Khám trực tiếp & mang theo kết quả');
22
+ });
23
+
24
+ it('leaves native chat content unchanged', () => {
25
+ expect(
26
+ getChatMessageDisplayContent({
27
+ content: 'Keep &amp; exactly as authored',
28
+ metadata: {},
29
+ })
30
+ ).toBe('Keep &amp; exactly as authored');
31
+ });
32
+
33
+ it('returns decoded markup as text content rather than an HTML contract', () => {
34
+ expect(
35
+ getChatMessageDisplayContent({
36
+ content: '&lt;img src=x onerror=alert(1)&gt;',
37
+ metadata: { externalChat: true },
38
+ })
39
+ ).toBe('<img src=x onerror=alert(1)>');
40
+ });
41
+ });
@@ -0,0 +1,28 @@
1
+ import type { ChatMessage } from '@tuturuuu/internal-api';
2
+ import { decode } from 'html-entities';
3
+
4
+ const MAX_ENTITY_DECODE_PASSES = 3;
5
+
6
+ export function decodeExternalChatContent(
7
+ content: string,
8
+ externalChat: boolean
9
+ ) {
10
+ if (!externalChat) return content;
11
+
12
+ let displayContent = content;
13
+ for (let pass = 0; pass < MAX_ENTITY_DECODE_PASSES; pass += 1) {
14
+ const decoded = decode(displayContent, { level: 'html5', scope: 'body' });
15
+ if (decoded === displayContent) break;
16
+ displayContent = decoded;
17
+ }
18
+ return displayContent;
19
+ }
20
+
21
+ export function getChatMessageDisplayContent(
22
+ message: Pick<ChatMessage, 'content' | 'metadata'>
23
+ ) {
24
+ return decodeExternalChatContent(
25
+ message.content,
26
+ message.metadata.externalChat === true
27
+ );
28
+ }
@@ -26,6 +26,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
26
26
  import { toast } from '../sonner';
27
27
  import { AiMessageParts } from './ai-message-parts';
28
28
  import { getAiMessagePartsFromMetadata } from './ai-message-render-utils';
29
+ import { getChatMessageDisplayContent } from './external-message-content';
29
30
  import { MessageAttachmentButton } from './message-attachment-button';
30
31
  import { MessageLinkPreviews, MessageText } from './message-links';
31
32
  import {
@@ -259,6 +260,7 @@ function MessageContent({
259
260
  const t = useTranslations('chat');
260
261
  const aiParts = getAiMessagePartsFromMetadata(message.metadata);
261
262
  const externalAttachment = readExternalAttachment(message.metadata);
263
+ const displayContent = getChatMessageDisplayContent(message);
262
264
 
263
265
  return (
264
266
  <div
@@ -284,10 +286,10 @@ function MessageContent({
284
286
  textFallback={message.content}
285
287
  />
286
288
  ) : (
287
- <MessageText content={message.content} />
289
+ <MessageText content={displayContent} />
288
290
  )}
289
291
  <MessageLinkPreviews
290
- content={message.content}
292
+ content={displayContent}
291
293
  conversationId={message.conversationId}
292
294
  isOwnMessage={isOwnMessage}
293
295
  wsId={wsId}