Compare commits
5 Commits
7f960b026b
...
f589182709
| Author | SHA1 | Date | |
|---|---|---|---|
| f589182709 | |||
| ef573376ac | |||
| 34d9272790 | |||
| 96f7187031 | |||
| 664dcd4cd8 |
+5
-1
@@ -37,6 +37,10 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
|
|||||||
| P4-8 | Encrypted-search cache (opt-in toggle, clear button, logout wipe) | `utils/searchCache.ts`, message-search | enable in search panel → search → reload → coverage persists; logout wipes |
|
| P4-8 | Encrypted-search cache (opt-in toggle, clear button, logout wipe) | `utils/searchCache.ts`, message-search | enable in search panel → search → reload → coverage persists; logout wipes |
|
||||||
| N97a | Session blob migration + cross-tab logout sync | `state/sessions.ts`, `useSessionSync` | login on old build → new build migrates; logout in tab A → tab B drops to auth |
|
| N97a | Session blob migration + cross-tab logout sync | `state/sessions.ts`, `useSessionSync` | login on old build → new build migrates; logout in tab A → tab B drops to auth |
|
||||||
| P4-1 | Slack-style thread notifications (participating default, All/Mentions/Mute, badge math) | `utils/threadNotifications.ts`, `ClientNonUIFeatures`, `roomToUnread` | 6-step checklist in LOTUS_TODO §P4-1 |
|
| P4-1 | Slack-style thread notifications (participating default, All/Mentions/Mute, badge math) | `utils/threadNotifications.ts`, `ClientNonUIFeatures`, `roomToUnread` | 6-step checklist in LOTUS_TODO §P4-1 |
|
||||||
|
| AW-1 | Scheduled-message cancel no longer ghost-sends (error row on failure) | `ScheduledMessagesTray.tsx` | schedule → cancel with network cut → item stays + error; retry works |
|
||||||
|
| AW-2 | Emoji lazy-load (search/autocomplete/recents fill in; board opens fast) | `plugins/emoji.ts` + consumers | first emoji-board open of a session: grid+search populate; reactions still label |
|
||||||
|
| AW-3 | SW precache (repeat-visit near-instant; deploys still picked up immediately) | `sw.ts`, `vite.config.js` | load app twice (2nd = cached assets); deploy → reload picks new version |
|
||||||
|
| AW-4 | Desktop CSP tighten + Escape/panel fixes + thread Jump to Latest | `tauri.conf.json`, Room/ThreadPanel | desktop: boots, avatars/media load, VT323 font renders, location maps embed, calls connect, deep links work |
|
||||||
|
|
||||||
**Verified working in live testing (2026-06):** A2, B1–B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression.
|
**Verified working in live testing (2026-06):** A2, B1–B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression.
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@ retry … AbortError: Restart delayed event timed out before the HS responded`,
|
|||||||
|
|
||||||
### Dependencies & Build
|
### Dependencies & Build
|
||||||
|
|
||||||
- **`matrix-js-sdk` pinned to a Release Candidate** (`41.6.0-rc.0`); `@atlaskit` and build tools (`vite`, `typescript`, `eslint`) on unstable/experimental pins — review for stable versions; RC SDK is a tree-shaking/bundle-size risk.
|
- ~~**`matrix-js-sdk` pinned to a Release Candidate**~~ — **done (2026-07):** moved to `41.7.0` stable (crypto-wasm 18.3.1 security bump). Deep-audit dep triage: all 16 npm advisories are dev-only/unreachable/dead-dep — zero shipped exposure; dead `dompurify` removed. `@atlaskit`/build-tool pins remain review-worthy but low priority.
|
||||||
- **Build-time overhead:** `lotusDenoise` does heavy sequential `fs` work in `closeBundle`; `viteStaticCopy` config is complex with redundant renames — could be streamlined.
|
- **Build-time overhead:** `lotusDenoise` does heavy sequential `fs` work in `closeBundle`; `viteStaticCopy` config is complex with redundant renames — could be streamlined.
|
||||||
|
|
||||||
### Code Hygiene / DevEx
|
### Code Hygiene / DevEx
|
||||||
|
|||||||
Generated
+20
-40
@@ -24,7 +24,6 @@
|
|||||||
"@tanstack/react-query": "5.100.13",
|
"@tanstack/react-query": "5.100.13",
|
||||||
"@tanstack/react-query-devtools": "5.100.13",
|
"@tanstack/react-query-devtools": "5.100.13",
|
||||||
"@tanstack/react-virtual": "3.13.25",
|
"@tanstack/react-virtual": "3.13.25",
|
||||||
"@types/dompurify": "3.2.0",
|
|
||||||
"@workadventure/noise-suppression": "0.0.4",
|
"@workadventure/noise-suppression": "0.0.4",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
"badwords-list": "2.0.1-4",
|
"badwords-list": "2.0.1-4",
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
"dayjs": "1.11.20",
|
"dayjs": "1.11.20",
|
||||||
"deepfilternet3-noise-filter": "1.2.1",
|
"deepfilternet3-noise-filter": "1.2.1",
|
||||||
"domhandler": "6.0.1",
|
"domhandler": "6.0.1",
|
||||||
"dompurify": "3.4.5",
|
|
||||||
"emojibase": "17.0.0",
|
"emojibase": "17.0.0",
|
||||||
"emojibase-data": "17.0.0",
|
"emojibase-data": "17.0.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
@@ -54,7 +52,7 @@
|
|||||||
"katex": "0.16.11",
|
"katex": "0.16.11",
|
||||||
"linkify-react": "4.3.3",
|
"linkify-react": "4.3.3",
|
||||||
"linkifyjs": "4.3.3",
|
"linkifyjs": "4.3.3",
|
||||||
"matrix-js-sdk": "41.6.0-rc.0",
|
"matrix-js-sdk": "41.7.0",
|
||||||
"matrix-widget-api": "1.17.0",
|
"matrix-widget-api": "1.17.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "5.7.284",
|
"pdfjs-dist": "5.7.284",
|
||||||
@@ -75,7 +73,8 @@
|
|||||||
"slate-history": "0.113.1",
|
"slate-history": "0.113.1",
|
||||||
"slate-react": "0.124.2",
|
"slate-react": "0.124.2",
|
||||||
"styled-components": "6.4.2",
|
"styled-components": "6.4.2",
|
||||||
"ua-parser-js": "2.0.10"
|
"ua-parser-js": "2.0.10",
|
||||||
|
"workbox-precaching": "7.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
|
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
|
||||||
@@ -2697,9 +2696,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
"version": "18.3.0",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.1.tgz",
|
||||||
"integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==",
|
"integrity": "sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
@@ -3920,16 +3919,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/dompurify": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==",
|
|
||||||
"deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"dompurify": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -4051,7 +4040,7 @@
|
|||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"devOptional": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/ua-parser-js": {
|
"node_modules/@types/ua-parser-js": {
|
||||||
"version": "0.7.39",
|
"version": "0.7.39",
|
||||||
@@ -5550,12 +5539,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/content-type": {
|
"node_modules/content-type": {
|
||||||
"version": "1.0.5",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/conventional-commit-types": {
|
"node_modules/conventional-commit-types": {
|
||||||
@@ -6196,15 +6189,6 @@
|
|||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
|
||||||
"version": "3.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz",
|
|
||||||
"integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==",
|
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@types/trusted-types": "^2.0.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
@@ -9971,16 +9955,16 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk": {
|
"node_modules/matrix-js-sdk": {
|
||||||
"version": "41.6.0-rc.0",
|
"version": "41.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.6.0-rc.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.7.0.tgz",
|
||||||
"integrity": "sha512-FcTQyR+Nfh0ASEogYcX393hxGr1936Esg53Z+0f9O4SBsAxl1ZSkLXY3JfLZRLX9dNe38VVwQDQE6QuwnwV7Zw==",
|
"integrity": "sha512-MP0xNv/VVRbshq00TE6EVo77IIXsQk0KjiVtgKV0t9j/V77a6Klt00QrrO0XykkTUsNC0+mQeBMxnx75rZO86Q==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^18.3.1",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^2.0.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
@@ -13228,7 +13212,6 @@
|
|||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz",
|
||||||
"integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==",
|
"integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/workbox-expiration": {
|
"node_modules/workbox-expiration": {
|
||||||
@@ -13269,7 +13252,6 @@
|
|||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz",
|
||||||
"integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==",
|
"integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"workbox-core": "7.4.1",
|
"workbox-core": "7.4.1",
|
||||||
@@ -13306,7 +13288,6 @@
|
|||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz",
|
||||||
"integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==",
|
"integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"workbox-core": "7.4.1"
|
"workbox-core": "7.4.1"
|
||||||
@@ -13316,7 +13297,6 @@
|
|||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz",
|
||||||
"integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==",
|
"integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"workbox-core": "7.4.1"
|
"workbox-core": "7.4.1"
|
||||||
|
|||||||
+3
-4
@@ -49,7 +49,6 @@
|
|||||||
"@tanstack/react-query": "5.100.13",
|
"@tanstack/react-query": "5.100.13",
|
||||||
"@tanstack/react-query-devtools": "5.100.13",
|
"@tanstack/react-query-devtools": "5.100.13",
|
||||||
"@tanstack/react-virtual": "3.13.25",
|
"@tanstack/react-virtual": "3.13.25",
|
||||||
"@types/dompurify": "3.2.0",
|
|
||||||
"@workadventure/noise-suppression": "0.0.4",
|
"@workadventure/noise-suppression": "0.0.4",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
"badwords-list": "2.0.1-4",
|
"badwords-list": "2.0.1-4",
|
||||||
@@ -61,7 +60,6 @@
|
|||||||
"dayjs": "1.11.20",
|
"dayjs": "1.11.20",
|
||||||
"deepfilternet3-noise-filter": "1.2.1",
|
"deepfilternet3-noise-filter": "1.2.1",
|
||||||
"domhandler": "6.0.1",
|
"domhandler": "6.0.1",
|
||||||
"dompurify": "3.4.5",
|
|
||||||
"emojibase": "17.0.0",
|
"emojibase": "17.0.0",
|
||||||
"emojibase-data": "17.0.0",
|
"emojibase-data": "17.0.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
@@ -79,7 +77,7 @@
|
|||||||
"katex": "0.16.11",
|
"katex": "0.16.11",
|
||||||
"linkify-react": "4.3.3",
|
"linkify-react": "4.3.3",
|
||||||
"linkifyjs": "4.3.3",
|
"linkifyjs": "4.3.3",
|
||||||
"matrix-js-sdk": "41.6.0-rc.0",
|
"matrix-js-sdk": "41.7.0",
|
||||||
"matrix-widget-api": "1.17.0",
|
"matrix-widget-api": "1.17.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "5.7.284",
|
"pdfjs-dist": "5.7.284",
|
||||||
@@ -100,7 +98,8 @@
|
|||||||
"slate-history": "0.113.1",
|
"slate-history": "0.113.1",
|
||||||
"slate-react": "0.124.2",
|
"slate-react": "0.124.2",
|
||||||
"styled-components": "6.4.2",
|
"styled-components": "6.4.2",
|
||||||
"ua-parser-js": "2.0.10"
|
"ua-parser-js": "2.0.10",
|
||||||
|
"workbox-precaching": "7.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
|
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo } from 'react';
|
import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo, useState } from 'react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { Box, MenuItem, Text, toRem } from 'folds';
|
import { Box, MenuItem, Text, toRem } from 'folds';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
@@ -11,7 +11,7 @@ import { onTabPress } from '../../../utils/keyboard';
|
|||||||
import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils';
|
import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils';
|
||||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||||
import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
|
import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
|
||||||
import { IEmoji, emojis } from '../../../plugins/emoji';
|
import { IEmoji, emojis, loadEmojiData } from '../../../plugins/emoji';
|
||||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
@@ -47,13 +47,32 @@ export function EmoticonAutocomplete({
|
|||||||
const imagePacks = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms);
|
const imagePacks = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms);
|
||||||
const recentEmoji = useRecentEmoji(mx, 20);
|
const recentEmoji = useRecentEmoji(mx, 20);
|
||||||
|
|
||||||
|
// Lazily load emojibase data (see plugins/emoji `loadEmojiData`). Until it
|
||||||
|
// resolves, `emojis` is empty and autocomplete matches only custom-emoji
|
||||||
|
// packs; the unicode emoji list fills in once loaded.
|
||||||
|
const [loadedEmojis, setLoadedEmojis] = useState<IEmoji[]>(() => emojis);
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
loadEmojiData()
|
||||||
|
// Fresh array reference: loadEmojiData populates the module-level array
|
||||||
|
// IN PLACE, so state set to the same ref would bail out of re-rendering
|
||||||
|
// and the search list would never gain the unicode emojis.
|
||||||
|
.then((loaded) => {
|
||||||
|
if (alive) setLoadedEmojis(loaded.emojis.slice());
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const searchList = useMemo(() => {
|
const searchList = useMemo(() => {
|
||||||
const list: Array<EmoticonSearchItem> = [];
|
const list: Array<EmoticonSearchItem> = [];
|
||||||
return list.concat(
|
return list.concat(
|
||||||
imagePacks.flatMap((pack) => pack.getImages(ImageUsage.Emoticon)),
|
imagePacks.flatMap((pack) => pack.getImages(ImageUsage.Emoticon)),
|
||||||
emojis,
|
loadedEmojis,
|
||||||
);
|
);
|
||||||
}, [imagePacks]);
|
}, [imagePacks, loadedEmojis]);
|
||||||
|
|
||||||
const [result, search, resetSearch] = useAsyncSearch(
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
searchList,
|
searchList,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Box, config, Icons, Scroll } from 'folds';
|
import { Box, config, Icons, Scroll } from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
@@ -15,7 +16,7 @@ import { isKeyHotkey } from 'is-hotkey';
|
|||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
|
import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji';
|
import { EmojiData, IEmoji, emojiGroups, emojis, loadEmojiData } from '../../plugins/emoji';
|
||||||
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
|
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
|
||||||
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
|
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
|
||||||
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
|
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
|
||||||
@@ -56,6 +57,33 @@ import { VirtualTile } from '../virtualizer';
|
|||||||
const RECENT_GROUP_ID = 'recent_group';
|
const RECENT_GROUP_ID = 'recent_group';
|
||||||
const SEARCH_GROUP_ID = 'search_group';
|
const SEARCH_GROUP_ID = 'search_group';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily pull in the emojibase data (see plugins/emoji `loadEmojiData`). The
|
||||||
|
* `emojis`/`emojiGroups` arrays are populated in place once the promise
|
||||||
|
* resolves; we wrap them in a fresh object on load so React re-renders and the
|
||||||
|
* board fills in. Before that, both are empty and the board shows only custom
|
||||||
|
* image packs / recents (which is fleeting — the load starts on mount).
|
||||||
|
*/
|
||||||
|
const useEmojiData = (): EmojiData => {
|
||||||
|
const [data, setData] = useState<EmojiData>(() => ({ emojis, emojiGroups }));
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
loadEmojiData()
|
||||||
|
// Fresh array references (not just a fresh wrapper): downstream memos
|
||||||
|
// depend on the arrays themselves, which are populated IN PLACE — same
|
||||||
|
// refs would skip recompute and leave emoji search empty until remount.
|
||||||
|
.then((loaded) => {
|
||||||
|
if (alive)
|
||||||
|
setData({ emojis: loaded.emojis.slice(), emojiGroups: loaded.emojiGroups.slice() });
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
type EmojiGroupItem = {
|
type EmojiGroupItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -75,6 +103,7 @@ const useGroups = (
|
|||||||
|
|
||||||
const recentEmojis = useRecentEmoji(mx, 21);
|
const recentEmojis = useRecentEmoji(mx, 21);
|
||||||
const labels = useEmojiGroupLabels();
|
const labels = useEmojiGroupLabels();
|
||||||
|
const { emojiGroups: loadedEmojiGroups } = useEmojiData();
|
||||||
|
|
||||||
const emojiGroupItems = useMemo(() => {
|
const emojiGroupItems = useMemo(() => {
|
||||||
const g: EmojiGroupItem[] = [];
|
const g: EmojiGroupItem[] = [];
|
||||||
@@ -99,7 +128,7 @@ const useGroups = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
emojiGroups.forEach((group) => {
|
loadedEmojiGroups.forEach((group) => {
|
||||||
g.push({
|
g.push({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: labels[group.id],
|
name: labels[group.id],
|
||||||
@@ -108,7 +137,7 @@ const useGroups = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}, [mx, recentEmojis, labels, imagePacks, tab]);
|
}, [mx, recentEmojis, labels, imagePacks, tab, loadedEmojiGroups]);
|
||||||
|
|
||||||
const stickerGroupItems = useMemo(() => {
|
const stickerGroupItems = useMemo(() => {
|
||||||
const g: StickerGroupItem[] = [];
|
const g: StickerGroupItem[] = [];
|
||||||
@@ -177,6 +206,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
|
|||||||
const usage = ImageUsage.Emoticon;
|
const usage = ImageUsage.Emoticon;
|
||||||
const labels = useEmojiGroupLabels();
|
const labels = useEmojiGroupLabels();
|
||||||
const icons = useEmojiGroupIcons();
|
const icons = useEmojiGroupIcons();
|
||||||
|
const { emojiGroups: loadedEmojiGroups } = useEmojiData();
|
||||||
|
|
||||||
const packLabels = useMemo(() => {
|
const packLabels = useMemo(() => {
|
||||||
const map = new Map<string, string | undefined>();
|
const map = new Map<string, string | undefined>();
|
||||||
@@ -234,7 +264,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SidebarDivider />
|
<SidebarDivider />
|
||||||
{emojiGroups.map((group) => (
|
{loadedEmojiGroups.map((group) => (
|
||||||
<GroupIcon
|
<GroupIcon
|
||||||
key={group.id}
|
key={group.id}
|
||||||
active={activeGroupId === group.id}
|
active={activeGroupId === group.id}
|
||||||
@@ -409,13 +439,14 @@ export function EmojiBoard({
|
|||||||
const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks);
|
const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks);
|
||||||
const groups = emojiTab ? emojiGroupItems : stickerGroupItems;
|
const groups = emojiTab ? emojiGroupItems : stickerGroupItems;
|
||||||
const renderItem = useItemRenderer(tab);
|
const renderItem = useItemRenderer(tab);
|
||||||
|
const { emojis: loadedEmojis } = useEmojiData();
|
||||||
|
|
||||||
const searchList = useMemo(() => {
|
const searchList = useMemo(() => {
|
||||||
let list: Array<PackImageReader | IEmoji> = [];
|
let list: Array<PackImageReader | IEmoji> = [];
|
||||||
list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage)));
|
list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage)));
|
||||||
if (emojiTab) list = list.concat(emojis);
|
if (emojiTab) list = list.concat(loadedEmojis);
|
||||||
return list;
|
return list;
|
||||||
}, [emojiTab, usage, imagePacks]);
|
}, [emojiTab, usage, imagePacks, loadedEmojis]);
|
||||||
|
|
||||||
const [result, search, resetSearch] = useAsyncSearch(
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
searchList,
|
searchList,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Box, Line } from 'folds';
|
import { Box, Line } from 'folds';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
@@ -49,15 +49,46 @@ export function Room() {
|
|||||||
useCallback(
|
useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (isKeyHotkey('escape', evt)) {
|
if (isKeyHotkey('escape', evt)) {
|
||||||
|
// Skip when a composer already consumed Escape (it preventDefaults).
|
||||||
|
if (evt.defaultPrevented) return;
|
||||||
|
// Skip while a thread panel is open: listener registration order
|
||||||
|
// means this can run BEFORE the panel's own Escape handler, and the
|
||||||
|
// user's intent there is "close the panel", not "mark room read".
|
||||||
|
if (activeThreadId) return;
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(mx, room.roomId, hideActivity);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mx, room.roomId, hideActivity],
|
[mx, room.roomId, hideActivity, activeThreadId],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
|
const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
|
||||||
|
|
||||||
|
// Thread panel and media gallery are mutually exclusive on every screen size:
|
||||||
|
// opening one closes the other. Detect the just-opened transition so whichever
|
||||||
|
// was opened most recently wins.
|
||||||
|
const prevThreadRef = useRef(activeThreadId);
|
||||||
|
const prevGalleryRef = useRef(galleryOpen);
|
||||||
|
useEffect(() => {
|
||||||
|
const threadJustOpened = Boolean(activeThreadId) && !prevThreadRef.current;
|
||||||
|
const galleryJustOpened = galleryOpen && !prevGalleryRef.current;
|
||||||
|
if (threadJustOpened && galleryOpen) {
|
||||||
|
setGalleryOpen(false);
|
||||||
|
} else if (galleryJustOpened && activeThreadId) {
|
||||||
|
setActiveThreadId(null);
|
||||||
|
}
|
||||||
|
prevThreadRef.current = activeThreadId;
|
||||||
|
prevGalleryRef.current = galleryOpen;
|
||||||
|
}, [activeThreadId, galleryOpen, setGalleryOpen, setActiveThreadId]);
|
||||||
|
|
||||||
|
// On non-desktop screens at most one right-side panel may show, priority
|
||||||
|
// thread > gallery > members. On desktop thread + members may coexist while
|
||||||
|
// thread + gallery stay mutually exclusive (via the effect above).
|
||||||
|
const isDesktop = screenSize === ScreenSize.Desktop;
|
||||||
|
const showThreadPanel = !callView && Boolean(activeThreadId);
|
||||||
|
const showGallery = !callView && galleryOpen && (isDesktop || !activeThreadId);
|
||||||
|
const showMembers = !callView && isDrawer && (isDesktop || (!activeThreadId && !galleryOpen));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PowerLevelsContextProvider value={powerLevels}>
|
<PowerLevelsContextProvider value={powerLevels}>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
@@ -86,7 +117,7 @@ export function Room() {
|
|||||||
<CallChatView />
|
<CallChatView />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!callView && galleryOpen && (
|
{showGallery && (
|
||||||
<>
|
<>
|
||||||
{screenSize === ScreenSize.Desktop && (
|
{screenSize === ScreenSize.Desktop && (
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
@@ -94,7 +125,7 @@ export function Room() {
|
|||||||
<MediaGallery key={room.roomId} room={room} onClose={() => setGalleryOpen(false)} />
|
<MediaGallery key={room.roomId} room={room} onClose={() => setGalleryOpen(false)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!callView && activeThreadId && (
|
{showThreadPanel && activeThreadId && (
|
||||||
<>
|
<>
|
||||||
{screenSize === ScreenSize.Desktop && (
|
{screenSize === ScreenSize.Desktop && (
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
@@ -107,7 +138,7 @@ export function Room() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!callView && isDrawer && (
|
{showMembers && (
|
||||||
<>
|
<>
|
||||||
{screenSize === ScreenSize.Desktop && (
|
{screenSize === ScreenSize.Desktop && (
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
|
|||||||
@@ -679,15 +679,23 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
if (isKeyHotkey('escape', evt)) {
|
if (isKeyHotkey('escape', evt)) {
|
||||||
evt.preventDefault();
|
// Only consume Escape (and stop it bubbling to the thread panel / room
|
||||||
|
// window handlers) when the composer actually has something to dismiss.
|
||||||
|
// If we did nothing, let Escape propagate so those handlers can run.
|
||||||
if (autocompleteQuery) {
|
if (autocompleteQuery) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
setAutocompleteQuery(undefined);
|
setAutocompleteQuery(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (replyDraft) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
setReplyDraft(undefined);
|
setReplyDraft(undefined);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing],
|
[submit, replyDraft, setReplyDraft, enterForNewline, autocompleteQuery, isComposing],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
const [scheduledMessages, setScheduledMessages] = useAtom(scheduledMessagesAtom);
|
const [scheduledMessages, setScheduledMessages] = useAtom(scheduledMessagesAtom);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [cancelling, setCancelling] = useState<Set<string>>(new Set());
|
const [cancelling, setCancelling] = useState<Set<string>>(new Set());
|
||||||
|
const [cancelErrors, setCancelErrors] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const messages = useMemo(() => scheduledMessages.get(roomId) ?? [], [scheduledMessages, roomId]);
|
const messages = useMemo(() => scheduledMessages.get(roomId) ?? [], [scheduledMessages, roomId]);
|
||||||
|
|
||||||
@@ -68,12 +69,17 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
async (msg: ScheduledMessage) => {
|
async (msg: ScheduledMessage) => {
|
||||||
if (cancelling.has(msg.delayId)) return;
|
if (cancelling.has(msg.delayId)) return;
|
||||||
setCancelling((prev) => new Set(prev).add(msg.delayId));
|
setCancelling((prev) => new Set(prev).add(msg.delayId));
|
||||||
|
setCancelErrors((prev) => {
|
||||||
|
if (!prev.has(msg.delayId)) return prev;
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(msg.delayId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await cancelScheduledMessage(mx, msg.delayId);
|
await cancelScheduledMessage(mx, msg.delayId);
|
||||||
} catch {
|
// Only prune local state once the server confirms cancellation. If we
|
||||||
// If cancellation fails on the server, still remove locally
|
// removed it optimistically the still-live delayed event would fire and
|
||||||
// since the user intends to remove it
|
// the "cancelled" message would send anyway.
|
||||||
} finally {
|
|
||||||
setScheduledMessages((prev) => {
|
setScheduledMessages((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
const current = next.get(roomId) ?? [];
|
const current = next.get(roomId) ?? [];
|
||||||
@@ -85,6 +91,11 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
// Keep the item (still cancellable) and surface an inline error; the
|
||||||
|
// delayed event is still scheduled on the server.
|
||||||
|
setCancelErrors((prev) => new Set(prev).add(msg.delayId));
|
||||||
|
} finally {
|
||||||
setCancelling((prev) => {
|
setCancelling((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.delete(msg.delayId);
|
next.delete(msg.delayId);
|
||||||
@@ -131,13 +142,13 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<Box
|
<Box
|
||||||
key={msg.delayId}
|
key={msg.delayId}
|
||||||
alignItems="Center"
|
direction="Column"
|
||||||
gap="200"
|
|
||||||
style={{
|
style={{
|
||||||
padding: `${config.space.S100} ${config.space.S300}`,
|
padding: `${config.space.S100} ${config.space.S300}`,
|
||||||
borderTop: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
borderTop: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box alignItems="Center" gap="200">
|
||||||
<Text
|
<Text
|
||||||
size="T200"
|
size="T200"
|
||||||
priority="400"
|
priority="400"
|
||||||
@@ -148,7 +159,9 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{typeof msg.content.body === 'string' ? (msg.content.body as string) : '(message)'}
|
{typeof msg.content.body === 'string'
|
||||||
|
? (msg.content.body as string)
|
||||||
|
: '(message)'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" priority="300" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
|
<Text size="T200" priority="300" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||||
{formatSendAt(msg.sendAt)}
|
{formatSendAt(msg.sendAt)}
|
||||||
@@ -167,6 +180,15 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
|||||||
<Icon src={Icons.Cross} size="50" />
|
<Icon src={Icons.Cross} size="50" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
{cancelErrors.has(msg.delayId) && (
|
||||||
|
<Text
|
||||||
|
size="T200"
|
||||||
|
style={{ color: color.Critical.Main, paddingTop: config.space.S100 }}
|
||||||
|
>
|
||||||
|
Could not cancel this message. Try again.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
color,
|
||||||
config,
|
config,
|
||||||
Dialog,
|
Dialog,
|
||||||
Header,
|
Header,
|
||||||
@@ -43,8 +44,14 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
|||||||
const modalStyle = useModalStyle(320);
|
const modalStyle = useModalStyle(320);
|
||||||
const { addReminder } = useReminders();
|
const { addReminder } = useReminders();
|
||||||
const presets = useMemo(() => getPresets(), []);
|
const presets = useMemo(() => getPresets(), []);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handlePick = async (ms: number) => {
|
const handlePick = async (ms: number) => {
|
||||||
|
if (busy) return;
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
await addReminder({
|
await addReminder({
|
||||||
roomId,
|
roomId,
|
||||||
eventId,
|
eventId,
|
||||||
@@ -52,6 +59,10 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
|||||||
message: previewText || 'Reminder',
|
message: previewText || 'Reminder',
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setBusy(false);
|
||||||
|
setError('Could not set reminder. Try again.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,6 +119,7 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
|||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
|
disabled={busy}
|
||||||
onClick={() => handlePick(p.ms)}
|
onClick={() => handlePick(p.ms)}
|
||||||
>
|
>
|
||||||
<Text size="B300" truncate>
|
<Text size="B300" truncate>
|
||||||
@@ -115,6 +127,14 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
|||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
{error && (
|
||||||
|
<Text
|
||||||
|
size="T200"
|
||||||
|
style={{ color: color.Critical.Main, paddingTop: config.space.S100 }}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps)
|
|||||||
useCallback(
|
useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (isKeyHotkey('escape', evt)) {
|
if (isKeyHotkey('escape', evt)) {
|
||||||
|
// The composer preventDefaults Escape when it consumes it (dismissing
|
||||||
|
// autocomplete / clearing a reply draft). Don't close the panel in
|
||||||
|
// that case — only when Escape wasn't already handled.
|
||||||
|
if (evt.defaultPrevented) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
requestClose();
|
requestClose();
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ export const ThreadTimelineContent = style({
|
|||||||
padding: `${config.space.S400} 0`,
|
padding: `${config.space.S400} 0`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ThreadTimelineFloat = style({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: config.space.S400,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 1,
|
||||||
|
minWidth: 'max-content',
|
||||||
|
});
|
||||||
|
|
||||||
export const ThreadCentered = style({
|
export const ThreadCentered = style({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
padding: config.space.S700,
|
padding: config.space.S700,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { Editor } from 'slate';
|
|||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { Badge, Box, Line, Scroll, Spinner, Text, color, config } from 'folds';
|
import { Badge, Box, Chip, Icon, Icons, Line, Scroll, Spinner, Text, color, config } from 'folds';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../../utils/matrix';
|
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../../utils/matrix';
|
||||||
@@ -459,6 +459,14 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
|||||||
}
|
}
|
||||||
}, [scrollToBottomCount]);
|
}, [scrollToBottomCount]);
|
||||||
|
|
||||||
|
const handleJumpToBottom = useCallback(() => {
|
||||||
|
scrollToBottomRef.current.count += 1;
|
||||||
|
scrollToBottomRef.current.smooth = true;
|
||||||
|
// Flip atBottom so the layout effect re-runs (count re-read) and live
|
||||||
|
// events resume sticking to the bottom.
|
||||||
|
setAtBottom(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Scroll in-place editor into view.
|
// Scroll in-place editor into view.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editId) {
|
if (editId) {
|
||||||
@@ -949,6 +957,19 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
|||||||
<span ref={atBottomAnchorRef} />
|
<span ref={atBottomAnchorRef} />
|
||||||
</Box>
|
</Box>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
|
{!atBottom && (
|
||||||
|
<Box className={css.ThreadTimelineFloat} justifyContent="Center" alignItems="Center">
|
||||||
|
<Chip
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
radii="Pill"
|
||||||
|
outlined
|
||||||
|
before={<Icon size="50" src={Icons.ArrowBottom} />}
|
||||||
|
onClick={handleJumpToBottom}
|
||||||
|
>
|
||||||
|
<Text size="L400">Jump to Latest</Text>
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{editHistoryEvent && (
|
{editHistoryEvent && (
|
||||||
<EditHistoryModal
|
<EditHistoryModal
|
||||||
room={room}
|
room={room}
|
||||||
|
|||||||
@@ -2,11 +2,26 @@ import { useEffect, useState } from 'react';
|
|||||||
import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
||||||
import { getRecentEmojis } from '../plugins/recent-emoji';
|
import { getRecentEmojis } from '../plugins/recent-emoji';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
import { IEmoji } from '../plugins/emoji';
|
import { IEmoji, loadEmojiData } from '../plugins/emoji';
|
||||||
|
|
||||||
export const useRecentEmoji = (mx: MatrixClient, limit?: number): IEmoji[] => {
|
export const useRecentEmoji = (mx: MatrixClient, limit?: number): IEmoji[] => {
|
||||||
const [recentEmoji, setRecentEmoji] = useState(() => getRecentEmojis(mx, limit));
|
const [recentEmoji, setRecentEmoji] = useState(() => getRecentEmojis(mx, limit));
|
||||||
|
|
||||||
|
// Recent emojis are resolved against the (now lazily loaded) emojibase data
|
||||||
|
// via getRecentEmojis. Recompute once loadEmojiData has populated it so the
|
||||||
|
// recent list fills in on first open.
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
loadEmojiData()
|
||||||
|
.then(() => {
|
||||||
|
if (alive) setRecentEmoji(getRecentEmojis(mx, limit));
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, [mx, limit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleAccountData = (event: MatrixEvent) => {
|
const handleAccountData = (event: MatrixEvent) => {
|
||||||
if (event.getType() !== AccountDataEvent.ElementRecentEmoji) return;
|
if (event.getType() !== AccountDataEvent.ElementRecentEmoji) return;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { CallControl } from './CallControl';
|
import { CallControl } from './CallControl';
|
||||||
import { CallControlState } from './CallControlState';
|
import { CallControlState } from './CallControlState';
|
||||||
|
import { verifyDenoiseAssets } from './denoiseSmokeCheck';
|
||||||
|
|
||||||
// Maximum time to wait for the embedded Element Call iframe to progress from
|
// Maximum time to wait for the embedded Element Call iframe to progress from
|
||||||
// initial load to a ready/joined state. If it hasn't by then, we assume the
|
// initial load to a ready/joined state. If it hasn't by then, we assume the
|
||||||
@@ -205,6 +206,12 @@ export class CallEmbed {
|
|||||||
params.append('lotusModel', denoiseModel);
|
params.append('lotusModel', denoiseModel);
|
||||||
params.append('lotusGate', denoiseGate.toString());
|
params.append('lotusGate', denoiseGate.toString());
|
||||||
params.append('lotusGateThreshold', denoiseGateThreshold.toString());
|
params.append('lotusGateThreshold', denoiseGateThreshold.toString());
|
||||||
|
|
||||||
|
// [lotus] Fire-and-forget: confirm the fork's ML-denoise assets are
|
||||||
|
// actually served under public/element-call/denoise/ (they're copied by
|
||||||
|
// vite.config.js at build time). Warns once if the copy step regressed;
|
||||||
|
// never blocks call start.
|
||||||
|
verifyDenoiseAssets(denoiseModel).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CallEmbed.startingCall(intent)) {
|
if (CallEmbed.startingCall(intent)) {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { trimTrailingSlash } from '../../utils/common';
|
||||||
|
|
||||||
|
// Denoise assets copied into public/element-call/denoise/ by vite.config.js's
|
||||||
|
// lotusDenoise() plugin. The filenames here MUST match what that plugin writes
|
||||||
|
// (and what the fork's TrackProcessor fetches at runtime). Grouped per model so
|
||||||
|
// the smoke-check only probes what the active call will actually load.
|
||||||
|
const DENOISE_ASSETS: Record<string, readonly string[]> = {
|
||||||
|
rnnoise: ['rnnoiseWorklet.js', 'rnnoise.wasm', 'rnnoise_simd.wasm'],
|
||||||
|
speex: ['speexWorklet.js', 'speex.wasm'],
|
||||||
|
dtln: ['workadventure/audio-worklet.js'],
|
||||||
|
deepfilternet: [
|
||||||
|
'deepfilternet/index.esm.js',
|
||||||
|
'deepfilternet/v2/pkg/df_bg.wasm',
|
||||||
|
'deepfilternet/v2/models/DeepFilterNet3_onnx.tar.gz',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// The noise-gate worklet is a shared asset the build ships for every model
|
||||||
|
// (loaded when the gate is enabled), so probe it regardless of the model.
|
||||||
|
const SHARED_ASSETS: readonly string[] = ['noiseGateWorklet.js'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire-and-forget smoke-check for the ML-denoise asset contract.
|
||||||
|
*
|
||||||
|
* The fork's in-source denoiser (lotusDenoiseSource) loads its worklet/wasm/ESM
|
||||||
|
* from `public/element-call/denoise/` at runtime; if the build's asset copy
|
||||||
|
* step regressed, those fetches 404 and denoise silently degrades to a raw mic.
|
||||||
|
* This HEAD-fetches the critical assets for the selected model and emits a
|
||||||
|
* single console.warn listing any that are missing. No UI, no throw — purely a
|
||||||
|
* developer/operator breadcrumb.
|
||||||
|
*
|
||||||
|
* @param model the selected denoise model (defaults to rnnoise)
|
||||||
|
* @returns true if every probed asset responded OK, false otherwise
|
||||||
|
*/
|
||||||
|
export async function verifyDenoiseAssets(model = 'rnnoise'): Promise<boolean> {
|
||||||
|
const base = new URL(
|
||||||
|
`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/denoise/`,
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
const names = [...(DENOISE_ASSETS[model] ?? DENOISE_ASSETS.rnnoise), ...SHARED_ASSETS];
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
names.map(async (name): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(new URL(name, base).href, { method: 'HEAD' });
|
||||||
|
return res.ok ? null : name;
|
||||||
|
} catch {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const missing = results.filter((n): n is string => n !== null);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[lotus-denoise] ML denoise assets missing under ${base.href} (model="${model}"): ${missing.join(
|
||||||
|
', ',
|
||||||
|
)} — the in-source denoiser will fall back to a raw mic. Check vite.config.js lotusDenoise().`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+98
-51
@@ -1,7 +1,4 @@
|
|||||||
import { CompactEmoji, fromUnicodeToHexcode } from 'emojibase';
|
import type { CompactEmoji } from 'emojibase';
|
||||||
import emojisData from 'emojibase-data/en/compact.json';
|
|
||||||
import joypixels from 'emojibase-data/en/shortcodes/joypixels.json';
|
|
||||||
import emojibase from 'emojibase-data/en/shortcodes/emojibase.json';
|
|
||||||
|
|
||||||
export type IEmoji = CompactEmoji & {
|
export type IEmoji = CompactEmoji & {
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
@@ -24,57 +21,76 @@ export type IEmojiGroup = {
|
|||||||
emojis: IEmoji[];
|
emojis: IEmoji[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcodesFor = (hexcode: string): string[] | string | undefined =>
|
export type EmojiData = {
|
||||||
joypixels[hexcode] || emojibase[hexcode];
|
emojis: IEmoji[];
|
||||||
|
emojiGroups: IEmojiGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShortcodeMap = Record<string, string | string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PERF (lazy emojibase split): the heavy `emojibase-data` JSON (compact emoji
|
||||||
|
* data + the joypixels/emojibase shortcode maps, ~965 KB combined) used to be
|
||||||
|
* imported statically at module top-level. Because reaction/message rendering
|
||||||
|
* (`Reaction`, `scaleSystemEmoji`) import this module eagerly, that dragged the
|
||||||
|
* whole `emojibase` chunk into the initial (eager) bundle graph.
|
||||||
|
*
|
||||||
|
* It is now loaded on demand via `loadEmojiData()` (a memoized dynamic import).
|
||||||
|
* Only lazy emoji surfaces (EmojiBoard, EmoticonAutocomplete, recent-emoji)
|
||||||
|
* trigger the load. Anything that renders eagerly (reaction/emoji tooltips and
|
||||||
|
* aria-labels via `getShortcodeFor`) gracefully degrades to `undefined` until
|
||||||
|
* the data has been loaded — the visible emoji glyph itself never depended on
|
||||||
|
* this data, so on-screen UX is unchanged; the shortcode label simply resolves
|
||||||
|
* once emoji data is loaded. `getHexcodeForEmoji` is inlined below so it stays
|
||||||
|
* synchronous WITHOUT pulling the `emojibase` runtime into the eager graph.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Inlined from emojibase's `fromUnicodeToHexcode` so this synchronous helper
|
||||||
|
// does not import the `emojibase` package (and thus the emojibase chunk) into
|
||||||
|
// the eager graph. Kept byte-for-byte behaviourally identical.
|
||||||
|
const SEQUENCE_REMOVAL_PATTERN = /200D|FE0E|FE0F/g;
|
||||||
|
|
||||||
|
export const getHexcodeForEmoji = (unicode: string, strip = true): string => {
|
||||||
|
const hexcode: string[] = [];
|
||||||
|
[...unicode].forEach((codepoint) => {
|
||||||
|
let hex = codepoint.codePointAt(0)?.toString(16).toUpperCase() ?? '';
|
||||||
|
while (hex.length < 4) {
|
||||||
|
hex = `0${hex}`;
|
||||||
|
}
|
||||||
|
if (!strip || !hex.match(SEQUENCE_REMOVAL_PATTERN)) {
|
||||||
|
hexcode.push(hex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hexcode.join('-');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populated by loadEmojiData(); `undefined` until the data has been loaded.
|
||||||
|
let joypixelsShortcodes: ShortcodeMap | undefined;
|
||||||
|
let emojibaseShortcodes: ShortcodeMap | undefined;
|
||||||
|
|
||||||
|
export const getShortcodesFor = (hexcode: string): string[] | string | undefined => {
|
||||||
|
if (!joypixelsShortcodes || !emojibaseShortcodes) return undefined;
|
||||||
|
return joypixelsShortcodes[hexcode] || emojibaseShortcodes[hexcode];
|
||||||
|
};
|
||||||
|
|
||||||
export const getShortcodeFor = (hexcode: string): string | undefined => {
|
export const getShortcodeFor = (hexcode: string): string | undefined => {
|
||||||
const shortcode = joypixels[hexcode] || emojibase[hexcode];
|
const shortcode = getShortcodesFor(hexcode);
|
||||||
return Array.isArray(shortcode) ? shortcode[0] : shortcode;
|
return Array.isArray(shortcode) ? shortcode[0] : shortcode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHexcodeForEmoji = fromUnicodeToHexcode;
|
// Shared, stable array references. They start empty and are populated in place
|
||||||
|
// the first time loadEmojiData() resolves (mirroring the previous eager module
|
||||||
|
// side-effect). React consumers await loadEmojiData() and re-render to observe
|
||||||
|
// the populated data; non-React consumers (recent-emoji) read them after load.
|
||||||
export const emojiGroups: IEmojiGroup[] = [
|
export const emojiGroups: IEmojiGroup[] = [
|
||||||
{
|
{ id: EmojiGroupId.People, order: 0, emojis: [] },
|
||||||
id: EmojiGroupId.People,
|
{ id: EmojiGroupId.Nature, order: 1, emojis: [] },
|
||||||
order: 0,
|
{ id: EmojiGroupId.Food, order: 2, emojis: [] },
|
||||||
emojis: [],
|
{ id: EmojiGroupId.Activity, order: 3, emojis: [] },
|
||||||
},
|
{ id: EmojiGroupId.Travel, order: 4, emojis: [] },
|
||||||
{
|
{ id: EmojiGroupId.Object, order: 5, emojis: [] },
|
||||||
id: EmojiGroupId.Nature,
|
{ id: EmojiGroupId.Symbol, order: 6, emojis: [] },
|
||||||
order: 1,
|
{ id: EmojiGroupId.Flag, order: 7, emojis: [] },
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Food,
|
|
||||||
order: 2,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Activity,
|
|
||||||
order: 3,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Travel,
|
|
||||||
order: 4,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Object,
|
|
||||||
order: 5,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Symbol,
|
|
||||||
order: 6,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EmojiGroupId.Flag,
|
|
||||||
order: 7,
|
|
||||||
emojis: [],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const emojis: IEmoji[] = [];
|
export const emojis: IEmoji[] = [];
|
||||||
@@ -95,7 +111,26 @@ function getGroupIndex(emoji: IEmoji): number | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
emojisData.forEach((emoji) => {
|
let emojiDataPromise: Promise<EmojiData> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily load emojibase data (dynamic import → the `emojibase` chunk). Memoized:
|
||||||
|
* the JSON is fetched/parsed and `emojis`/`emojiGroups` are built exactly once.
|
||||||
|
*/
|
||||||
|
export const loadEmojiData = (): Promise<EmojiData> => {
|
||||||
|
if (!emojiDataPromise) {
|
||||||
|
emojiDataPromise = (async (): Promise<EmojiData> => {
|
||||||
|
const [emojisModule, joypixelsModule, emojibaseModule] = await Promise.all([
|
||||||
|
import('emojibase-data/en/compact.json'),
|
||||||
|
import('emojibase-data/en/shortcodes/joypixels.json'),
|
||||||
|
import('emojibase-data/en/shortcodes/emojibase.json'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
joypixelsShortcodes = joypixelsModule.default as ShortcodeMap;
|
||||||
|
emojibaseShortcodes = emojibaseModule.default as ShortcodeMap;
|
||||||
|
|
||||||
|
const emojisData = emojisModule.default as unknown as CompactEmoji[];
|
||||||
|
emojisData.forEach((emoji) => {
|
||||||
const myShortCodes = getShortcodesFor(emoji.hexcode);
|
const myShortCodes = getShortcodesFor(emoji.hexcode);
|
||||||
if (!myShortCodes) return;
|
if (!myShortCodes) return;
|
||||||
if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return;
|
if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return;
|
||||||
@@ -111,4 +146,16 @@ emojisData.forEach((emoji) => {
|
|||||||
addEmojiToGroup(groupIndex, em);
|
addEmojiToGroup(groupIndex, em);
|
||||||
emojis.push(em);
|
emojis.push(em);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { emojis, emojiGroups };
|
||||||
|
})();
|
||||||
|
// Don't cache a rejection: a transient chunk-load failure (e.g. mid-deploy
|
||||||
|
// 404) would otherwise permanently disable emoji data until a full reload.
|
||||||
|
emojiDataPromise = emojiDataPromise.catch((err) => {
|
||||||
|
emojiDataPromise = undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return emojiDataPromise;
|
||||||
|
};
|
||||||
|
|||||||
@@ -577,12 +577,12 @@ export const getReactCustomHtmlParser = (
|
|||||||
return (
|
return (
|
||||||
<span className={css.EmoticonBase}>
|
<span className={css.EmoticonBase}>
|
||||||
<span className={css.Emoticon()}>
|
<span className={css.Emoticon()}>
|
||||||
<img {...props} className={css.EmoticonImg} src={htmlSrc} />
|
<img {...props} className={css.EmoticonImg} src={htmlSrc} loading="lazy" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (htmlSrc) return <img {...props} className={css.Img} src={htmlSrc} />;
|
if (htmlSrc) return <img {...props} className={css.Img} src={htmlSrc} loading="lazy" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,307 +2,33 @@ import React, { MutableRefObject, ReactNode, useEffect, useRef } from 'react';
|
|||||||
|
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
|
|
||||||
import 'prismjs/components/prism-abap.js';
|
// PERF: Prism used to import every bundled language (~574 KB lazy chunk). We now
|
||||||
import 'prismjs/components/prism-abnf.js';
|
// ship a curated subset covering the languages actually seen in chat. Imports
|
||||||
import 'prismjs/components/prism-actionscript.js';
|
// MUST stay in dependency order (Prism component files assume their base grammar
|
||||||
import 'prismjs/components/prism-ada.js';
|
// is already registered): base grammars (markup/css/clike/javascript) first,
|
||||||
import 'prismjs/components/prism-agda.js';
|
// then languages that extend them (e.g. c→cpp, javascript→typescript,
|
||||||
import 'prismjs/components/prism-al.js';
|
// markup+javascript→jsx, jsx+typescript→tsx, markup→markdown).
|
||||||
import 'prismjs/components/prism-antlr4.js';
|
import 'prismjs/components/prism-markup.js'; // markup / html / xml / svg
|
||||||
import 'prismjs/components/prism-apacheconf.js';
|
import 'prismjs/components/prism-css.js';
|
||||||
import 'prismjs/components/prism-apex.js';
|
|
||||||
import 'prismjs/components/prism-apl.js';
|
|
||||||
import 'prismjs/components/prism-applescript.js';
|
|
||||||
import 'prismjs/components/prism-aql.js';
|
|
||||||
import 'prismjs/components/prism-arff.js';
|
|
||||||
import 'prismjs/components/prism-armasm.js';
|
|
||||||
import 'prismjs/components/prism-arturo.js';
|
|
||||||
import 'prismjs/components/prism-asciidoc.js';
|
|
||||||
import 'prismjs/components/prism-asm6502.js';
|
|
||||||
import 'prismjs/components/prism-asmatmel.js';
|
|
||||||
import 'prismjs/components/prism-aspnet.js';
|
|
||||||
import 'prismjs/components/prism-autohotkey.js';
|
|
||||||
import 'prismjs/components/prism-autoit.js';
|
|
||||||
import 'prismjs/components/prism-avisynth.js';
|
|
||||||
import 'prismjs/components/prism-avro-idl.js';
|
|
||||||
import 'prismjs/components/prism-awk.js';
|
|
||||||
import 'prismjs/components/prism-bash.js';
|
|
||||||
import 'prismjs/components/prism-basic.js';
|
|
||||||
import 'prismjs/components/prism-batch.js';
|
|
||||||
import 'prismjs/components/prism-bbcode.js';
|
|
||||||
import 'prismjs/components/prism-bbj.js';
|
|
||||||
import 'prismjs/components/prism-bicep.js';
|
|
||||||
import 'prismjs/components/prism-birb.js';
|
|
||||||
import 'prismjs/components/prism-bnf.js';
|
|
||||||
import 'prismjs/components/prism-bqn.js';
|
|
||||||
import 'prismjs/components/prism-brainfuck.js';
|
|
||||||
import 'prismjs/components/prism-brightscript.js';
|
|
||||||
import 'prismjs/components/prism-bro.js';
|
|
||||||
import 'prismjs/components/prism-bsl.js';
|
|
||||||
import 'prismjs/components/prism-c.js';
|
|
||||||
import 'prismjs/components/prism-cfscript.js';
|
|
||||||
import 'prismjs/components/prism-cil.js';
|
|
||||||
import 'prismjs/components/prism-cilkc.js';
|
|
||||||
import 'prismjs/components/prism-cilkcpp.js';
|
|
||||||
import 'prismjs/components/prism-clike.js';
|
import 'prismjs/components/prism-clike.js';
|
||||||
import 'prismjs/components/prism-clojure.js';
|
import 'prismjs/components/prism-javascript.js'; // js
|
||||||
import 'prismjs/components/prism-cmake.js';
|
import 'prismjs/components/prism-json.js';
|
||||||
import 'prismjs/components/prism-cobol.js';
|
import 'prismjs/components/prism-yaml.js';
|
||||||
import 'prismjs/components/prism-coffeescript.js';
|
import 'prismjs/components/prism-bash.js'; // bash / shell / sh
|
||||||
import 'prismjs/components/prism-concurnas.js';
|
import 'prismjs/components/prism-python.js';
|
||||||
import 'prismjs/components/prism-cooklang.js';
|
import 'prismjs/components/prism-rust.js';
|
||||||
import 'prismjs/components/prism-coq.js';
|
import 'prismjs/components/prism-go.js';
|
||||||
|
import 'prismjs/components/prism-java.js';
|
||||||
|
import 'prismjs/components/prism-c.js';
|
||||||
import 'prismjs/components/prism-cpp.js';
|
import 'prismjs/components/prism-cpp.js';
|
||||||
import 'prismjs/components/prism-csharp.js';
|
import 'prismjs/components/prism-csharp.js';
|
||||||
import 'prismjs/components/prism-cshtml.js';
|
|
||||||
import 'prismjs/components/prism-csp.js';
|
|
||||||
import 'prismjs/components/prism-css-extras.js';
|
|
||||||
import 'prismjs/components/prism-css.js';
|
|
||||||
import 'prismjs/components/prism-csv.js';
|
|
||||||
import 'prismjs/components/prism-cue.js';
|
|
||||||
import 'prismjs/components/prism-cypher.js';
|
|
||||||
import 'prismjs/components/prism-d.js';
|
|
||||||
import 'prismjs/components/prism-dart.js';
|
|
||||||
import 'prismjs/components/prism-dataweave.js';
|
|
||||||
import 'prismjs/components/prism-dax.js';
|
|
||||||
import 'prismjs/components/prism-dhall.js';
|
|
||||||
import 'prismjs/components/prism-diff.js';
|
|
||||||
import 'prismjs/components/prism-dns-zone-file.js';
|
|
||||||
import 'prismjs/components/prism-docker.js';
|
|
||||||
import 'prismjs/components/prism-dot.js';
|
|
||||||
import 'prismjs/components/prism-ebnf.js';
|
|
||||||
import 'prismjs/components/prism-editorconfig.js';
|
|
||||||
import 'prismjs/components/prism-eiffel.js';
|
|
||||||
import 'prismjs/components/prism-ejs.js';
|
|
||||||
import 'prismjs/components/prism-elixir.js';
|
|
||||||
import 'prismjs/components/prism-elm.js';
|
|
||||||
import 'prismjs/components/prism-erb.js';
|
|
||||||
import 'prismjs/components/prism-erlang.js';
|
|
||||||
import 'prismjs/components/prism-etlua.js';
|
|
||||||
import 'prismjs/components/prism-excel-formula.js';
|
|
||||||
import 'prismjs/components/prism-factor.js';
|
|
||||||
import 'prismjs/components/prism-false.js';
|
|
||||||
import 'prismjs/components/prism-firestore-security-rules.js';
|
|
||||||
import 'prismjs/components/prism-flow.js';
|
|
||||||
import 'prismjs/components/prism-fortran.js';
|
|
||||||
import 'prismjs/components/prism-fsharp.js';
|
|
||||||
import 'prismjs/components/prism-ftl.js';
|
|
||||||
import 'prismjs/components/prism-gap.js';
|
|
||||||
import 'prismjs/components/prism-gcode.js';
|
|
||||||
import 'prismjs/components/prism-gdscript.js';
|
|
||||||
import 'prismjs/components/prism-gedcom.js';
|
|
||||||
import 'prismjs/components/prism-gettext.js';
|
|
||||||
import 'prismjs/components/prism-gherkin.js';
|
|
||||||
import 'prismjs/components/prism-git.js';
|
|
||||||
import 'prismjs/components/prism-glsl.js';
|
|
||||||
import 'prismjs/components/prism-gml.js';
|
|
||||||
import 'prismjs/components/prism-gn.js';
|
|
||||||
import 'prismjs/components/prism-go-module.js';
|
|
||||||
import 'prismjs/components/prism-go.js';
|
|
||||||
import 'prismjs/components/prism-gradle.js';
|
|
||||||
import 'prismjs/components/prism-graphql.js';
|
|
||||||
import 'prismjs/components/prism-groovy.js';
|
|
||||||
import 'prismjs/components/prism-haml.js';
|
|
||||||
import 'prismjs/components/prism-handlebars.js';
|
|
||||||
import 'prismjs/components/prism-haskell.js';
|
|
||||||
import 'prismjs/components/prism-haxe.js';
|
|
||||||
import 'prismjs/components/prism-hcl.js';
|
|
||||||
import 'prismjs/components/prism-hlsl.js';
|
|
||||||
import 'prismjs/components/prism-hoon.js';
|
|
||||||
import 'prismjs/components/prism-hpkp.js';
|
|
||||||
import 'prismjs/components/prism-hsts.js';
|
|
||||||
import 'prismjs/components/prism-http.js';
|
|
||||||
import 'prismjs/components/prism-ichigojam.js';
|
|
||||||
import 'prismjs/components/prism-icon.js';
|
|
||||||
import 'prismjs/components/prism-icu-message-format.js';
|
|
||||||
import 'prismjs/components/prism-idris.js';
|
|
||||||
import 'prismjs/components/prism-iecst.js';
|
|
||||||
import 'prismjs/components/prism-ignore.js';
|
|
||||||
import 'prismjs/components/prism-inform7.js';
|
|
||||||
import 'prismjs/components/prism-ini.js';
|
|
||||||
import 'prismjs/components/prism-io.js';
|
|
||||||
import 'prismjs/components/prism-j.js';
|
|
||||||
import 'prismjs/components/prism-java.js';
|
|
||||||
import 'prismjs/components/prism-javadoclike.js';
|
|
||||||
import 'prismjs/components/prism-javascript.js';
|
|
||||||
import 'prismjs/components/prism-javastacktrace.js';
|
|
||||||
import 'prismjs/components/prism-jexl.js';
|
|
||||||
import 'prismjs/components/prism-jolie.js';
|
|
||||||
import 'prismjs/components/prism-jq.js';
|
|
||||||
import 'prismjs/components/prism-js-extras.js';
|
|
||||||
import 'prismjs/components/prism-js-templates.js';
|
|
||||||
import 'prismjs/components/prism-json.js';
|
|
||||||
import 'prismjs/components/prism-json5.js';
|
|
||||||
import 'prismjs/components/prism-jsonp.js';
|
|
||||||
import 'prismjs/components/prism-jsstacktrace.js';
|
|
||||||
import 'prismjs/components/prism-jsx.js';
|
|
||||||
import 'prismjs/components/prism-julia.js';
|
|
||||||
import 'prismjs/components/prism-keepalived.js';
|
|
||||||
import 'prismjs/components/prism-keyman.js';
|
|
||||||
import 'prismjs/components/prism-kotlin.js';
|
|
||||||
import 'prismjs/components/prism-kumir.js';
|
|
||||||
import 'prismjs/components/prism-kusto.js';
|
|
||||||
import 'prismjs/components/prism-latex.js';
|
|
||||||
import 'prismjs/components/prism-latte.js';
|
|
||||||
import 'prismjs/components/prism-less.js';
|
|
||||||
import 'prismjs/components/prism-lilypond.js';
|
|
||||||
import 'prismjs/components/prism-linker-script.js';
|
|
||||||
import 'prismjs/components/prism-liquid.js';
|
|
||||||
import 'prismjs/components/prism-lisp.js';
|
|
||||||
import 'prismjs/components/prism-livescript.js';
|
|
||||||
import 'prismjs/components/prism-llvm.js';
|
|
||||||
import 'prismjs/components/prism-log.js';
|
|
||||||
import 'prismjs/components/prism-lolcode.js';
|
|
||||||
import 'prismjs/components/prism-lua.js';
|
|
||||||
import 'prismjs/components/prism-magma.js';
|
|
||||||
import 'prismjs/components/prism-makefile.js';
|
|
||||||
import 'prismjs/components/prism-markdown.js';
|
|
||||||
import 'prismjs/components/prism-markup-templating.js';
|
|
||||||
import 'prismjs/components/prism-markup.js';
|
|
||||||
import 'prismjs/components/prism-mata.js';
|
|
||||||
import 'prismjs/components/prism-matlab.js';
|
|
||||||
import 'prismjs/components/prism-maxscript.js';
|
|
||||||
import 'prismjs/components/prism-mel.js';
|
|
||||||
import 'prismjs/components/prism-mermaid.js';
|
|
||||||
import 'prismjs/components/prism-metafont.js';
|
|
||||||
import 'prismjs/components/prism-mizar.js';
|
|
||||||
import 'prismjs/components/prism-mongodb.js';
|
|
||||||
import 'prismjs/components/prism-monkey.js';
|
|
||||||
import 'prismjs/components/prism-moonscript.js';
|
|
||||||
import 'prismjs/components/prism-n1ql.js';
|
|
||||||
import 'prismjs/components/prism-n4js.js';
|
|
||||||
import 'prismjs/components/prism-nand2tetris-hdl.js';
|
|
||||||
import 'prismjs/components/prism-naniscript.js';
|
|
||||||
import 'prismjs/components/prism-nasm.js';
|
|
||||||
import 'prismjs/components/prism-neon.js';
|
|
||||||
import 'prismjs/components/prism-nevod.js';
|
|
||||||
import 'prismjs/components/prism-nginx.js';
|
|
||||||
import 'prismjs/components/prism-nim.js';
|
|
||||||
import 'prismjs/components/prism-nix.js';
|
|
||||||
import 'prismjs/components/prism-nsis.js';
|
|
||||||
import 'prismjs/components/prism-objectivec.js';
|
|
||||||
import 'prismjs/components/prism-ocaml.js';
|
|
||||||
import 'prismjs/components/prism-odin.js';
|
|
||||||
import 'prismjs/components/prism-opencl.js';
|
|
||||||
import 'prismjs/components/prism-openqasm.js';
|
|
||||||
import 'prismjs/components/prism-oz.js';
|
|
||||||
import 'prismjs/components/prism-parigp.js';
|
|
||||||
import 'prismjs/components/prism-parser.js';
|
|
||||||
import 'prismjs/components/prism-pascal.js';
|
|
||||||
import 'prismjs/components/prism-pascaligo.js';
|
|
||||||
import 'prismjs/components/prism-pcaxis.js';
|
|
||||||
import 'prismjs/components/prism-peoplecode.js';
|
|
||||||
import 'prismjs/components/prism-perl.js';
|
|
||||||
import 'prismjs/components/prism-php-extras.js';
|
|
||||||
import 'prismjs/components/prism-php.js';
|
|
||||||
import 'prismjs/components/prism-phpdoc.js';
|
|
||||||
import 'prismjs/components/prism-plant-uml.js';
|
|
||||||
import 'prismjs/components/prism-powerquery.js';
|
|
||||||
import 'prismjs/components/prism-powershell.js';
|
|
||||||
import 'prismjs/components/prism-processing.js';
|
|
||||||
import 'prismjs/components/prism-prolog.js';
|
|
||||||
import 'prismjs/components/prism-promql.js';
|
|
||||||
import 'prismjs/components/prism-properties.js';
|
|
||||||
import 'prismjs/components/prism-protobuf.js';
|
|
||||||
import 'prismjs/components/prism-psl.js';
|
|
||||||
import 'prismjs/components/prism-pug.js';
|
|
||||||
import 'prismjs/components/prism-puppet.js';
|
|
||||||
import 'prismjs/components/prism-pure.js';
|
|
||||||
import 'prismjs/components/prism-purebasic.js';
|
|
||||||
import 'prismjs/components/prism-purescript.js';
|
|
||||||
import 'prismjs/components/prism-python.js';
|
|
||||||
import 'prismjs/components/prism-q.js';
|
|
||||||
import 'prismjs/components/prism-qml.js';
|
|
||||||
import 'prismjs/components/prism-qore.js';
|
|
||||||
import 'prismjs/components/prism-qsharp.js';
|
|
||||||
import 'prismjs/components/prism-r.js';
|
|
||||||
import 'prismjs/components/prism-reason.js';
|
|
||||||
import 'prismjs/components/prism-regex.js';
|
|
||||||
import 'prismjs/components/prism-rego.js';
|
|
||||||
import 'prismjs/components/prism-renpy.js';
|
|
||||||
import 'prismjs/components/prism-rescript.js';
|
|
||||||
import 'prismjs/components/prism-rest.js';
|
|
||||||
import 'prismjs/components/prism-rip.js';
|
|
||||||
import 'prismjs/components/prism-roboconf.js';
|
|
||||||
import 'prismjs/components/prism-robotframework.js';
|
|
||||||
import 'prismjs/components/prism-ruby.js';
|
|
||||||
import 'prismjs/components/prism-rust.js';
|
|
||||||
import 'prismjs/components/prism-sas.js';
|
|
||||||
import 'prismjs/components/prism-sass.js';
|
|
||||||
import 'prismjs/components/prism-scala.js';
|
|
||||||
import 'prismjs/components/prism-scheme.js';
|
|
||||||
import 'prismjs/components/prism-scss.js';
|
|
||||||
import 'prismjs/components/prism-shell-session.js';
|
|
||||||
import 'prismjs/components/prism-smali.js';
|
|
||||||
import 'prismjs/components/prism-smalltalk.js';
|
|
||||||
import 'prismjs/components/prism-smarty.js';
|
|
||||||
import 'prismjs/components/prism-sml.js';
|
|
||||||
import 'prismjs/components/prism-solidity.js';
|
|
||||||
import 'prismjs/components/prism-solution-file.js';
|
|
||||||
import 'prismjs/components/prism-soy.js';
|
|
||||||
import 'prismjs/components/prism-splunk-spl.js';
|
|
||||||
import 'prismjs/components/prism-sqf.js';
|
|
||||||
import 'prismjs/components/prism-sql.js';
|
import 'prismjs/components/prism-sql.js';
|
||||||
import 'prismjs/components/prism-squirrel.js';
|
import 'prismjs/components/prism-diff.js';
|
||||||
import 'prismjs/components/prism-stan.js';
|
import 'prismjs/components/prism-docker.js';
|
||||||
import 'prismjs/components/prism-stata.js';
|
import 'prismjs/components/prism-markdown.js';
|
||||||
import 'prismjs/components/prism-stylus.js';
|
import 'prismjs/components/prism-typescript.js'; // ts
|
||||||
import 'prismjs/components/prism-supercollider.js';
|
import 'prismjs/components/prism-jsx.js';
|
||||||
import 'prismjs/components/prism-swift.js';
|
|
||||||
import 'prismjs/components/prism-systemd.js';
|
|
||||||
import 'prismjs/components/prism-t4-templating.js';
|
|
||||||
import 'prismjs/components/prism-t4-vb.js';
|
|
||||||
import 'prismjs/components/prism-tap.js';
|
|
||||||
import 'prismjs/components/prism-tcl.js';
|
|
||||||
import 'prismjs/components/prism-textile.js';
|
|
||||||
import 'prismjs/components/prism-toml.js';
|
|
||||||
import 'prismjs/components/prism-tremor.js';
|
|
||||||
import 'prismjs/components/prism-tsx.js';
|
import 'prismjs/components/prism-tsx.js';
|
||||||
import 'prismjs/components/prism-tt2.js';
|
|
||||||
import 'prismjs/components/prism-turtle.js';
|
|
||||||
import 'prismjs/components/prism-twig.js';
|
|
||||||
import 'prismjs/components/prism-typescript.js';
|
|
||||||
import 'prismjs/components/prism-typoscript.js';
|
|
||||||
import 'prismjs/components/prism-unrealscript.js';
|
|
||||||
import 'prismjs/components/prism-uorazor.js';
|
|
||||||
import 'prismjs/components/prism-uri.js';
|
|
||||||
import 'prismjs/components/prism-v.js';
|
|
||||||
import 'prismjs/components/prism-vala.js';
|
|
||||||
import 'prismjs/components/prism-vbnet.js';
|
|
||||||
import 'prismjs/components/prism-velocity.js';
|
|
||||||
import 'prismjs/components/prism-verilog.js';
|
|
||||||
import 'prismjs/components/prism-vhdl.js';
|
|
||||||
import 'prismjs/components/prism-vim.js';
|
|
||||||
import 'prismjs/components/prism-visual-basic.js';
|
|
||||||
import 'prismjs/components/prism-warpscript.js';
|
|
||||||
import 'prismjs/components/prism-wasm.js';
|
|
||||||
import 'prismjs/components/prism-web-idl.js';
|
|
||||||
import 'prismjs/components/prism-wgsl.js';
|
|
||||||
import 'prismjs/components/prism-wiki.js';
|
|
||||||
import 'prismjs/components/prism-wolfram.js';
|
|
||||||
import 'prismjs/components/prism-wren.js';
|
|
||||||
import 'prismjs/components/prism-xeora.js';
|
|
||||||
import 'prismjs/components/prism-xml-doc.js';
|
|
||||||
import 'prismjs/components/prism-xojo.js';
|
|
||||||
import 'prismjs/components/prism-xquery.js';
|
|
||||||
import 'prismjs/components/prism-yaml.js';
|
|
||||||
import 'prismjs/components/prism-yang.js';
|
|
||||||
import 'prismjs/components/prism-zig.js';
|
|
||||||
import 'prismjs/components/prism-arduino.js';
|
|
||||||
|
|
||||||
// Broken:
|
|
||||||
//
|
|
||||||
// import 'prismjs/components/prism-bison.js';
|
|
||||||
// import 'prismjs/components/prism-chaiscript.js';
|
|
||||||
// import 'prismjs/components/prism-core.js';
|
|
||||||
// import 'prismjs/components/prism-crystal.js';
|
|
||||||
// import 'prismjs/components/prism-django.js';
|
|
||||||
// import 'prismjs/components/prism-javadoc.js';
|
|
||||||
// import 'prismjs/components/prism-jsdoc.js';
|
|
||||||
// import 'prismjs/components/prism-plsql.js';
|
|
||||||
// import 'prismjs/components/prism-racket.js';
|
|
||||||
// import 'prismjs/components/prism-sparql.js';
|
|
||||||
// import 'prismjs/components/prism-t4-cs.js';
|
|
||||||
|
|
||||||
import './ReactPrism.css';
|
import './ReactPrism.css';
|
||||||
// using classNames .prism-dark .prism-light from ReactPrism.css
|
// using classNames .prism-dark .prism-light from ReactPrism.css
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
||||||
import { addRecentEmoji, getRecentEmojis, IRecentEmojiContent } from './recent-emoji';
|
import { addRecentEmoji, getRecentEmojis, IRecentEmojiContent } from './recent-emoji';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
import { emojis } from './emoji';
|
import { emojis, loadEmojiData } from './emoji';
|
||||||
|
|
||||||
// A Map-backed MatrixClient stub supporting get/setAccountData.
|
// A Map-backed MatrixClient stub supporting get/setAccountData.
|
||||||
const createMx = () => {
|
const createMx = () => {
|
||||||
@@ -25,6 +25,9 @@ const createMx = () => {
|
|||||||
const getStored = (store: Map<string, unknown>): IRecentEmojiContent['recent_emoji'] =>
|
const getStored = (store: Map<string, unknown>): IRecentEmojiContent['recent_emoji'] =>
|
||||||
(store.get(AccountDataEvent.ElementRecentEmoji) as IRecentEmojiContent | undefined)?.recent_emoji;
|
(store.get(AccountDataEvent.ElementRecentEmoji) as IRecentEmojiContent | undefined)?.recent_emoji;
|
||||||
|
|
||||||
|
// Emoji data is now loaded lazily; populate `emojis` before the round trips.
|
||||||
|
await loadEmojiData();
|
||||||
|
|
||||||
// Pick two real unicode emojis to drive add->get round trips.
|
// Pick two real unicode emojis to drive add->get round trips.
|
||||||
const u1 = emojis[0].unicode;
|
const u1 = emojis[0].unicode;
|
||||||
const u2 = emojis[1].unicode;
|
const u2 = emojis[1].unicode;
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
/// <reference lib="WebWorker" />
|
/// <reference lib="WebWorker" />
|
||||||
|
|
||||||
|
import { precacheAndRoute, type PrecacheEntry } from 'workbox-precaching';
|
||||||
|
|
||||||
export type {};
|
export type {};
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope & {
|
||||||
|
// Replaced at build time by vite-plugin-pwa (injectManifest) with the list of
|
||||||
|
// hashed build assets to precache. See vite.config.js VitePWA injectManifest.
|
||||||
|
__WB_MANIFEST: Array<string | PrecacheEntry>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PRECACHE (workbox-precaching). `self.__WB_MANIFEST` is replaced at build time
|
||||||
|
* by vite-plugin-pwa with the list of hashed build assets
|
||||||
|
* (assets/**\/*.{js,css,wasm}; see vite.config.js injectManifest.globPatterns).
|
||||||
|
*
|
||||||
|
* DEPLOY-SAFETY INVARIANTS (do not break):
|
||||||
|
* 1. index.html / navigations are NEVER precached or precache-routed. The
|
||||||
|
* manifest globs only `assets/**` (content-hashed), so index.html (served
|
||||||
|
* from the app root) is absent from it and navigation requests fall through
|
||||||
|
* to the network — a new deploy is picked up immediately, no stale SPA
|
||||||
|
* shell. We deliberately do NOT register a navigation route /
|
||||||
|
* createHandlerBoundToURL fallback.
|
||||||
|
* 2. precacheAndRoute only matches its own manifest URLs (same-origin hashed
|
||||||
|
* assets). It never matches the media-auth paths handled by the fetch
|
||||||
|
* listener below — those are cross-origin homeserver URLs absent from the
|
||||||
|
* manifest — so the existing media fetch behaviour is fully preserved. It
|
||||||
|
* is registered before that listener; for a media request the precache
|
||||||
|
* route finds no match and does not call respondWith, so the media handler
|
||||||
|
* still runs.
|
||||||
|
* 3. Assets are content-hashed, so a changed asset ships under a new filename;
|
||||||
|
* PrecacheController drops entries no longer in the current manifest on
|
||||||
|
* activate, so the precache self-updates each deploy without unbounded
|
||||||
|
* growth.
|
||||||
|
*/
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
type SessionInfo = {
|
type SessionInfo = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
|||||||
+13
-1
@@ -261,7 +261,19 @@ export default defineConfig({
|
|||||||
injectRegister: false,
|
injectRegister: false,
|
||||||
manifest: false,
|
manifest: false,
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
injectionPoint: undefined,
|
// PRECACHE (P5): emit `self.__WB_MANIFEST` into src/sw.ts so it can
|
||||||
|
// precacheAndRoute the hashed build assets. index.html is deliberately
|
||||||
|
// EXCLUDED from the manifest (globs only `assets/**`) so navigations
|
||||||
|
// stay network-first and a new deploy is picked up immediately — see
|
||||||
|
// the deploy-safety invariants documented in src/sw.ts.
|
||||||
|
injectionPoint: 'self.__WB_MANIFEST',
|
||||||
|
globPatterns: ['assets/**/*.{js,css,wasm}'],
|
||||||
|
// Assets are content-hashed, so the filename is the cache key — don't
|
||||||
|
// append a revision cache-busting param.
|
||||||
|
dontCacheBustURLsMatching: /assets\//,
|
||||||
|
// Raised above the 2 MB default so the ~5.5 MB matrix-sdk crypto wasm
|
||||||
|
// (hash-busted and hot on every session) is precached deliberately.
|
||||||
|
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
|
||||||
// codeSplitting: false is not yet supported by vite-plugin-pwa 1.3.0;
|
// codeSplitting: false is not yet supported by vite-plugin-pwa 1.3.0;
|
||||||
// the inlineDynamicImports deprecation warning from Vite is from pwa internal build
|
// the inlineDynamicImports deprecation warning from Vite is from pwa internal build
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user