From 2e2f6e3467f6f9135f1ed664234cd07c6c8decbc Mon Sep 17 00:00:00 2001 From: lan Date: Wed, 3 Jun 2026 10:31:46 +0800 Subject: [PATCH] refactor(message): improve message synchronization and hook reliability Refactor the message management system to address synchronization issues and improve performance through request deduplication and enhanced lifecycle management. - Implement in-flight promise tracking in `MessageSyncService` to prevent redundant network requests for conversations and messages. - Enhance `useMessages` hook with `useFocusEffect` to trigger automatic synchronization when a chat screen regains focus. - Add `forceSync` capability to `MessageManager` and `MessageSyncService` to allow manual overrides of loading states during re-entry. - Consolidate WebSocket synchronization logic in `WSMessageHandler` using a throttled and deduplicated trigger mechanism. - Clean up unused styles and deprecated hooks (`baseHooks.ts`, `bubbleStyles.ts`, `inputStyles.ts`). - Update `metro.config.js` and `package.json` to include `@expo/ui` and optimize resolver settings. --- metro.config.js | 3 - package-lock.json | 393 ++++++++------- package.json | 1 + .../components/ChatScreen/bubbleStyles.ts | 317 ------------ src/screens/profile/ChatSettingsScreen.tsx | 42 +- .../profile/VerificationSettingsScreen.tsx | 2 +- src/stores/message/MessageManager.ts | 9 +- src/stores/message/baseHooks.ts | 477 ------------------ src/stores/message/hooks.ts | 52 +- .../message/services/MessageSyncService.ts | 81 ++- .../message/services/WSMessageHandler.ts | 99 ++-- src/stores/message/types.ts | 2 +- src/utils/inputStyles.ts | 33 -- 13 files changed, 396 insertions(+), 1115 deletions(-) delete mode 100644 src/screens/message/components/ChatScreen/bubbleStyles.ts delete mode 100644 src/stores/message/baseHooks.ts delete mode 100644 src/utils/inputStyles.ts diff --git a/metro.config.js b/metro.config.js index f7b0c3f..41b74be 100644 --- a/metro.config.js +++ b/metro.config.js @@ -7,9 +7,6 @@ const config = getDefaultConfig(__dirname); // Add wasm asset support config.resolver.assetExts.push('wasm'); -// Fix for livekit-client event-target-shim import warning -config.resolver.unstable_enablePackageExports = false; - // Support TypeScript path aliases (@/ -> ./src/) config.resolver.alias = { '@': path.resolve(__dirname, 'src'), diff --git a/package-lock.json b/package-lock.json index e1529f1..25ea393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "with_you", "version": "0.0.2", "dependencies": { + "@expo/ui": "^56.0.15", "@expo/vector-icons": "^15.1.1", "@livekit/react-native": "^2.11.0", "@livekit/react-native-webrtc": "^144.1.0", @@ -101,20 +102,20 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -390,13 +391,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -561,12 +562,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1033,12 +1034,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1048,12 +1049,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1082,13 +1083,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1117,9 +1118,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3029,9 +3030,9 @@ } }, "node_modules/@react-native-community/cli/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "devOptional": true, "license": "ISC", "bin": { @@ -3566,12 +3567,12 @@ } }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.9.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/pako": { @@ -3636,9 +3637,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, "node_modules/@vscode/sudo-prompt": { @@ -3649,9 +3650,9 @@ "license": "MIT" }, "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "version": "0.8.13", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3704,12 +3705,15 @@ } }, "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", + "dependencies": { + "debug": "4" + }, "engines": { - "node": ">= 14" + "node": ">= 6.0.0" } }, "node_modules/anser": { @@ -3969,31 +3973,6 @@ "proxy-from-env": "^2.1.0" } }, - "node_modules/axios/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/axios/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.17", "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", @@ -4149,12 +4128,12 @@ } }, "node_modules/barcode-detector": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/barcode-detector/-/barcode-detector-3.1.1.tgz", - "integrity": "sha512-ghWlEAV93ZCUniO7Co3ih/01XPm+U30CV+NoPbO6Chj5lZzHydDAqKlrBEd+37TkoR+QTH3tnnwd8k8epGTfIg==", + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/barcode-detector/-/barcode-detector-3.1.3.tgz", + "integrity": "sha512-omL3/x26oU9jlR0gUQcGdXIjQtMlrUGKF7xRFO1RwrQkRkRU7WLz0mgQEsdUtYBm2uX3JH+HQLrKlyTS/BxZRw==", "license": "MIT", "dependencies": { - "zxing-wasm": "3.0.1" + "zxing-wasm": "3.0.3" } }, "node_modules/base64-js": { @@ -4178,9 +4157,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.9", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", - "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "version": "2.10.33", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4257,9 +4236,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4281,9 +4260,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -4300,11 +4279,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4431,9 +4410,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "version": "1.0.30001793", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "funding": [ { "type": "opencollective", @@ -5099,9 +5078,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.365", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.365.tgz", + "integrity": "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -5304,9 +5283,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7134,16 +7113,16 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/human-signals": { @@ -7851,9 +7830,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -7955,9 +7934,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8237,6 +8226,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8257,6 +8249,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8277,6 +8272,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8297,6 +8295,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8850,6 +8851,28 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/metro-cache/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/metro-cache/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/metro-config": { "version": "0.84.4", "resolved": "https://registry.npmmirror.com/metro-config/-/metro-config-0.84.4.tgz", @@ -9110,9 +9133,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -9184,12 +9207,12 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9320,9 +9343,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -9350,10 +9373,13 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" + "version": "2.0.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/node-stream-zip": { "version": "1.15.0", @@ -9740,9 +9766,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.5.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -9767,12 +9793,12 @@ } }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -9780,6 +9806,15 @@ "node": ">=10.4.0" } }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/pngjs": { "version": "3.4.0", "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-3.4.0.tgz", @@ -10069,9 +10104,9 @@ } }, "node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -10120,9 +10155,9 @@ } }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", "license": "MIT" }, "node_modules/react-native": { @@ -10295,9 +10330,9 @@ } }, "node_modules/react-native-reanimated/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10450,9 +10485,9 @@ } }, "node_modules/react-native/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10462,9 +10497,9 @@ } }, "node_modules/react-native/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -10678,9 +10713,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -11158,13 +11193,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -11294,9 +11329,9 @@ "license": "MIT" }, "node_modules/slugify": { - "version": "1.6.8", - "resolved": "https://registry.npmmirror.com/slugify/-/slugify-1.6.8.tgz", - "integrity": "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==", + "version": "1.6.9", + "resolved": "https://registry.npmmirror.com/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -11648,9 +11683,9 @@ } }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmmirror.com/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.48.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11945,9 +11980,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -12511,9 +12546,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -12604,22 +12639,22 @@ } }, "node_modules/zxing-wasm": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/zxing-wasm/-/zxing-wasm-3.0.1.tgz", - "integrity": "sha512-3CLj6iaGkpqPWXAB4pIWkFOR63MwqGekpMzaROFKto4dFowiPmLlC56KoMoOSXzqOCOpI5DAvMdB8ku2va6fUg==", + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/zxing-wasm/-/zxing-wasm-3.0.3.tgz", + "integrity": "sha512-DdOn/G5F+qvZELWeO5ZFFwcN611TfMybxPV0LUUoutUmiH2t47MZSB7gLV9O9YLhvudBdnzQNAoFOu4Xz8eOrQ==", "license": "MIT", "dependencies": { "@types/emscripten": "^1.41.5", - "type-fest": "^5.4.4" + "type-fest": "^5.6.0" }, "peerDependencies": { "@types/emscripten": ">=1.39.6" } }, "node_modules/zxing-wasm/node_modules/type-fest": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.5.0.tgz", - "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "version": "5.7.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.7.0.tgz", + "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" diff --git a/package.json b/package.json index dfd460c..cab4aec 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "ios:submit": "eas submit --platform ios --profile production" }, "dependencies": { + "@expo/ui": "^56.0.15", "@expo/vector-icons": "^15.1.1", "@livekit/react-native": "^2.11.0", "@livekit/react-native-webrtc": "^144.1.0", diff --git a/src/screens/message/components/ChatScreen/bubbleStyles.ts b/src/screens/message/components/ChatScreen/bubbleStyles.ts deleted file mode 100644 index 5e9ee93..0000000 --- a/src/screens/message/components/ChatScreen/bubbleStyles.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * 消息气泡样式工具 - * 参考 Element X 设计,实现动态圆角和现代化气泡样式 - */ - -import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; -import { spacing, type AppColors } from '../../../../theme'; -import { useChatSettingsStore } from '../../../../stores/settings'; - -// 默认圆角,但会从 store 读取 -export const BUBBLE_RADIUS = 16; - -// 获取动态圆角值 -export function useBubbleRadius(): number { - return useChatSettingsStore((s) => s.messageRadius); -} - -export function getBubbleColors(colors: AppColors, outgoingBubbleColor?: string) { - return { - outgoing: { - background: outgoingBubbleColor || colors.chat.bubbleOutgoing, - text: colors.chat.textPrimary, - }, - incoming: { - background: colors.chat.bubbleIncoming, - text: colors.chat.textPrimary, - }, - replyHighlight: { - background: colors.chat.replyTint, - borderLeft: colors.chat.replyBorder, - }, - }; -} - -export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last'; - -// 使用动态圆角的 hook -export function useBubbleBorderRadius(isMe: boolean, position: MessageGroupPosition): ViewStyle { - const radius = useBubbleRadius(); - - if (isMe) { - switch (position) { - case 'single': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'first': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'middle': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: 4, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'last': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: 4, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - } - } else { - switch (position) { - case 'single': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - case 'first': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: 4, - borderBottomRightRadius: radius, - }; - case 'middle': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: 4, - borderBottomRightRadius: radius, - }; - case 'last': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - } - } -} - -// 保持原有的静态函数用于兼容 -export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => { - const radius = BUBBLE_RADIUS; - - if (isMe) { - switch (position) { - case 'single': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'first': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'middle': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: 4, - borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, - }; - case 'last': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: 4, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - } - } else { - switch (position) { - case 'single': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - case 'first': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: 4, - borderBottomRightRadius: radius, - }; - case 'middle': - return { - borderTopLeftRadius: 4, - borderTopRightRadius: radius, - borderBottomLeftRadius: 4, - borderBottomRightRadius: radius, - }; - case 'last': - return { - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - }; - } - } -}; - -export const getMessageGroupPosition = ( - index: number, - messages: Array<{ sender_id: string }>, - currentUserId: string -): MessageGroupPosition => { - const currentMessage = messages[index]; - if (!currentMessage) return 'single'; - - const prevMessage = index > 0 ? messages[index - 1] : null; - const nextMessage = index < messages.length - 1 ? messages[index + 1] : null; - - const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id; - const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id; - - if (!isSameSenderAsPrev && !isSameSenderAsNext) { - return 'single'; - } else if (!isSameSenderAsPrev && isSameSenderAsNext) { - return 'first'; - } else if (isSameSenderAsPrev && isSameSenderAsNext) { - return 'middle'; - } else { - return 'last'; - } -}; - -export const shouldShowSenderInfo = ( - index: number, - messages: Array<{ sender_id: string }> -): boolean => { - if (index === 0) return true; - - const currentMessage = messages[index]; - const prevMessage = messages[index - 1]; - - return prevMessage.sender_id !== currentMessage.sender_id; -}; - -export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors, outgoingBubbleColor?: string): ViewStyle => { - const bc = getBubbleColors(colors, outgoingBubbleColor); - return { - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm + 4, - minWidth: 60, - maxWidth: '75%', - backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background, - shadowColor: colors.chat.shadow, - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.06, - shadowRadius: 2, - elevation: 2, - }; -}; - -// 使用动态字号的 hook -export function useChatFontSize(): number { - return useChatSettingsStore((s) => s.fontSize); -} - -export const getBubbleTextStyle = (isMe: boolean, colors: AppColors, customFontSize?: number): TextStyle => { - const bc = getBubbleColors(colors); - const fontSize = customFontSize ?? 16; - return { - color: isMe ? bc.outgoing.text : bc.incoming.text, - fontSize, - lineHeight: Math.round(fontSize * 1.4), - letterSpacing: 0.2, - fontWeight: '400', - }; -}; - -export function createBubbleStyles(colors: AppColors, customFontSize?: number, outgoingBubbleColor?: string) { - const bc = getBubbleColors(colors, outgoingBubbleColor); - const fontSize = customFontSize ?? 16; - return StyleSheet.create({ - bubble: { - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm + 4, - minWidth: 60, - }, - outgoing: { - backgroundColor: bc.outgoing.background, - }, - incoming: { - backgroundColor: bc.incoming.background, - }, - text: { - fontSize, - lineHeight: Math.round(fontSize * 1.4), - letterSpacing: 0.2, - fontWeight: '400', - }, - shadow: { - shadowColor: colors.chat.shadow, - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.06, - shadowRadius: 2, - elevation: 2, - }, - longPressShadow: { - shadowColor: colors.chat.shadow, - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.12, - shadowRadius: 8, - elevation: 6, - }, - replyHighlight: { - borderLeftWidth: 3, - borderLeftColor: colors.primary.main, - backgroundColor: bc.replyHighlight.background, - }, - recalled: { - backgroundColor: colors.chat.surfaceMuted, - borderWidth: 1, - borderColor: colors.chat.border, - borderStyle: 'dashed', - borderRadius: 12, - }, - systemNotice: { - alignItems: 'center', - marginVertical: spacing.sm, - }, - systemNoticeText: { - fontSize: 12, - color: colors.chat.textSecondary, - backgroundColor: colors.chat.surfaceMuted, - paddingHorizontal: spacing.md, - paddingVertical: spacing.xs, - borderRadius: 12, - overflow: 'hidden', - }, - }); -} - -export default { - BUBBLE_RADIUS, - getBubbleColors, - getBubbleBorderRadius, - getMessageGroupPosition, - shouldShowSenderInfo, - getBubbleBaseStyle, - getBubbleTextStyle, - createBubbleStyles, -}; diff --git a/src/screens/profile/ChatSettingsScreen.tsx b/src/screens/profile/ChatSettingsScreen.tsx index 8824277..931cc2b 100644 --- a/src/screens/profile/ChatSettingsScreen.tsx +++ b/src/screens/profile/ChatSettingsScreen.tsx @@ -1,4 +1,4 @@ -/** +/** * 聊天设置页 ChatSettingsScreen * 威友 - 聊天个性化设置 */ @@ -9,7 +9,6 @@ import { StyleSheet, TouchableOpacity, ScrollView, - Dimensions, PanResponder, } from 'react-native'; import { StatusBar } from 'expo-status-bar'; @@ -30,7 +29,6 @@ import { CHAT_THEMES, } from '../../stores/settings'; -const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_MAX_WIDTH = 720; interface SliderProps { @@ -43,7 +41,6 @@ interface SliderProps { showValue?: boolean; } -// 自定义滑块组件(支持拖动) const Slider: React.FC = ({ value, min, @@ -56,13 +53,13 @@ const Slider: React.FC = ({ const colors = useAppColors(); const styles = useMemo(() => createSliderStyles(colors), [colors]); const trackWidthRef = useRef(0); - + const percentage = isNaN(value) ? 0 : ((value - min) / (max - min)) * 100; const updateValue = useCallback((locationX: number) => { const width = trackWidthRef.current; if (width <= 0) return; - + const newPercentage = Math.max(0, Math.min(100, (locationX / width) * 100)); const newValue = Math.round(min + (newPercentage / 100) * (max - min)); onValueChange(newValue); @@ -90,7 +87,7 @@ const Slider: React.FC = ({ {showValue && {isNaN(value) ? 0 : value}} {rightLabel && {rightLabel}} - { const styles = useMemo(() => createStyles(colors), [colors]); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); - // 从 store 获取状态 const fontSize = useChatSettingsStore((s) => s.fontSize); const messageRadius = useChatSettingsStore((s) => s.messageRadius); const themeIndex = useChatSettingsStore((s) => s.themeIndex); @@ -281,14 +269,13 @@ export const ChatSettingsScreen: React.FC = () => { const currentTheme = CHAT_THEMES[themeIndex]; - // 渲染聊天预览 const renderChatPreview = () => ( 预览 { @@ -316,7 +303,6 @@ export const ChatSettingsScreen: React.FC = () => { ); - // 渲染字号滑块 const renderFontSizeSlider = () => ( 消息字号 @@ -333,12 +319,11 @@ export const ChatSettingsScreen: React.FC = () => { ); - // 渲染主题颜色选择 const renderThemeColors = () => ( 主题颜色 - @@ -366,7 +351,6 @@ export const ChatSettingsScreen: React.FC = () => { ); - // 渲染圆角滑块 const renderRadiusSlider = () => ( 消息圆角 @@ -385,9 +369,9 @@ export const ChatSettingsScreen: React.FC = () => { router.back()} /> - diff --git a/src/screens/profile/VerificationSettingsScreen.tsx b/src/screens/profile/VerificationSettingsScreen.tsx index 91924f6..bedfff5 100644 --- a/src/screens/profile/VerificationSettingsScreen.tsx +++ b/src/screens/profile/VerificationSettingsScreen.tsx @@ -115,7 +115,7 @@ export const VerificationSettingsScreen: React.FC = () => { contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} > - {/* 状态卡片 */} + {/* 状态卡片 */} { - return this.syncService.fetchMessages(conversationId, afterSeq); + async fetchMessages(conversationId: string, afterSeq?: number, force?: boolean): Promise { + return this.syncService.fetchMessages(conversationId, afterSeq, force); } async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise { @@ -343,6 +343,11 @@ class MessageManager { return; } + // forceSync 时清除可能残留的 loading 锁,防止 fetchMessages 被阻断 + if (options?.forceSync) { + useMessageStore.getState().setLoadingMessages(normalizedId, false); + } + const task = (async () => { // 确保会话数据在 store 中(JPUSH 通知进入时 store 可能还没有此会话) const store = useMessageStore.getState(); diff --git a/src/stores/message/baseHooks.ts b/src/stores/message/baseHooks.ts deleted file mode 100644 index 4ac1eba..0000000 --- a/src/stores/message/baseHooks.ts +++ /dev/null @@ -1,477 +0,0 @@ -/** - * 消息模块 React Hooks - * - * 提供React组件与消息状态管理的集成 - * 所有hooks都基于zustand store的selector机制 - * 确保组件能实时获取状态更新 - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useShallow } from 'zustand/react/shallow'; -import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; -import { useMessageStore, normalizeConversationId } from './store'; - -// ==================== useConversations - 获取会话列表 ==================== - -interface UseConversationsReturn { - conversations: ConversationResponse[]; - isLoading: boolean; - refresh: () => Promise; - loadMore: () => Promise; - hasMore: boolean; -} - -/** - * 获取会话列表 - * 用于 MessageListScreen 等需要显示会话列表的组件 - * 使用 Zustand selector 自动订阅状态变化 - */ -export function useConversations( - fetchConversations: (forceRefresh: boolean, source: string) => Promise, - loadMoreConversations: () => Promise, - canLoadMore: () => boolean, - initialize: () => Promise -): UseConversationsReturn { - // 使用 Zustand selector 直接订阅状态 - const conversations = useMessageStore(state => state.conversationList); - const isLoading = useMessageStore(state => state.isLoadingConversations); - const [hasMore, setHasMore] = useState(() => canLoadMore()); - - useEffect(() => { - // 初始化 - initialize().catch(error => { - console.error('[useConversations] 初始化失败:', error); - }); - - // 冷启动兜底:延迟做一次强制刷新 - const coldStartSyncTimer = setTimeout(() => { - fetchConversations(true, 'hooks-initial-refresh').catch(error => { - console.error('[useConversations] 冷启动强制刷新失败:', error); - }); - }, 1200); - - return () => { - clearTimeout(coldStartSyncTimer); - }; - }, []); - - // 监听 hasMore 变化 - useEffect(() => { - setHasMore(canLoadMore()); - }, [conversations, canLoadMore]); - - const refresh = useCallback(async () => { - await fetchConversations(true, 'hooks-manual-refresh'); - }, [fetchConversations]); - - const loadMore = useCallback(async () => { - await loadMoreConversations(); - }, [loadMoreConversations]); - - return { - conversations, - isLoading, - refresh, - loadMore, - hasMore, - }; -} - -// ==================== useMessages - 获取指定会话的消息 ==================== - -interface UseMessagesReturn { - messages: MessageResponse[]; - isLoading: boolean; - hasMore: boolean; - loadMore: () => Promise; - refresh: () => Promise; -} - -/** - * 获取指定会话的消息 - * 使用 Zustand selector 自动订阅消息变化 - */ -export function useMessages( - conversationId: string, - fetchMessages: (conversationId: string) => Promise, - loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise -): UseMessagesReturn { - const normalizedConversationId = normalizeConversationId(conversationId); - - // 使用 useShallow 避免每次返回新数组引用导致的无限循环 - const messages = useMessageStore( - useShallow(state => state.messagesMap.get(normalizedConversationId) || []) - ); - const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId)); - const getActiveConversation = useMessageStore.getState().getActiveConversation; - const setCurrentConversation = useMessageStore.getState().setCurrentConversation; - const [hasMore, setHasMore] = useState(true); - - useEffect(() => { - // 激活会话 - fetchMessages(normalizedConversationId).catch(error => { - console.error('[useMessages] 激活会话失败:', error); - }); - - return () => { - // 清理活动会话 - if (getActiveConversation() === normalizedConversationId) { - setCurrentConversation(null); - } - }; - }, [normalizedConversationId]); - - const loadMore = useCallback(async () => { - const currentMessages = messages; - if (currentMessages.length === 0) return; - - // 消息数组在 store 内保持 seq 升序,首项即最早消息 - const minSeq = currentMessages[0]?.seq; - if (minSeq === undefined) return; - - const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20); - setHasMore(loadedMessages.length > 0); - }, [conversationId, messages, loadMoreMessages]); - - const refresh = useCallback(async () => { - await fetchMessages(conversationId); - }, [conversationId, fetchMessages]); - - return { - messages, - isLoading: isLoadingMessages, - hasMore, - loadMore, - refresh, - }; -} - -// ==================== useSendMessage - 发送消息 ==================== - -interface UseSendMessageReturn { - sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise; - isSending: boolean; -} - -/** - * 发送消息 - */ -export function useSendMessage( - conversationId: string, - sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise -): UseSendMessageReturn { - const [isSending, setIsSending] = useState(false); - - const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => { - setIsSending(true); - try { - const message = await sendMessageService(conversationId, segments, options); - return message; - } catch (error) { - console.error('[useSendMessage] 发送消息失败:', error); - return null; - } finally { - setIsSending(false); - } - }, [conversationId, sendMessageService]); - - return { - sendMessage, - isSending, - }; -} - -// ==================== useMarkAsRead - 标记已读 ==================== - -interface UseMarkAsReadReturn { - markAsRead: (seq: number) => Promise; - markAllAsRead: () => Promise; -} - -/** - * 标记已读 - */ -export function useMarkAsRead( - conversationId: string, - markAsReadService: (conversationId: string, seq: number) => Promise, - markAllAsReadService: () => Promise -): UseMarkAsReadReturn { - const markAsRead = useCallback(async (seq: number) => { - await markAsReadService(conversationId, seq); - }, [conversationId, markAsReadService]); - - const markAllAsRead = useCallback(async () => { - await markAllAsReadService(); - }, [markAllAsReadService]); - - return { - markAsRead, - markAllAsRead, - }; -} - -// ==================== useUnreadCount - 获取未读数 ==================== - -interface UseUnreadCountReturn { - totalUnreadCount: number; - systemUnreadCount: number; - refresh: () => Promise; -} - -/** - * 获取未读数 - * 使用 Zustand selector 直接订阅未读数变化 - */ -export function useUnreadCount(fetchUnreadCount: () => Promise): UseUnreadCountReturn { - // 使用 Zustand selector 直接订阅未读数 - const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); - const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - - useEffect(() => { - // 初始获取 - fetchUnreadCount(); - }, []); - - const refresh = useCallback(async () => { - await fetchUnreadCount(); - }, [fetchUnreadCount]); - - return { - totalUnreadCount, - systemUnreadCount, - refresh, - }; -} - -// ==================== useSystemUnreadCount - 获取系统消息未读数 ==================== - -interface UseSystemUnreadCountReturn { - systemUnreadCount: number; - setSystemUnreadCount: (count: number) => void; - incrementSystemUnreadCount: () => void; - decrementSystemUnreadCount: (count?: number) => void; -} - -/** - * 获取系统消息未读数 - * 使用 Zustand selector 直接订阅 - */ -export function useSystemUnreadCount( - setSystemUnreadCountService: (count: number) => void, - incrementSystemUnreadCountService: () => void, - decrementSystemUnreadCountService: (count?: number) => void -): UseSystemUnreadCountReturn { - // 使用 Zustand selector 直接订阅 - const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - - const setSystemUnreadCount = useCallback((count: number) => { - setSystemUnreadCountService(count); - }, [setSystemUnreadCountService]); - - const incrementSystemUnreadCount = useCallback(() => { - incrementSystemUnreadCountService(); - }, [incrementSystemUnreadCountService]); - - const decrementSystemUnreadCount = useCallback((count = 1) => { - decrementSystemUnreadCountService(count); - }, [decrementSystemUnreadCountService]); - - return { - systemUnreadCount, - setSystemUnreadCount, - incrementSystemUnreadCount, - decrementSystemUnreadCount, - }; -} - -// ==================== useConversation - 获取单个会话 ==================== - -interface UseConversationReturn { - conversation: ConversationResponse | null; - refresh: () => Promise; -} - -/** - * 获取单个会话 - * 使用 Zustand selector 直接订阅会话变化 - */ -export function useConversation( - conversationId: string, - fetchConversationDetail: (conversationId: string) => Promise -): UseConversationReturn { - // 使用 useShallow 避免每次返回新对象引用导致的无限循环 - const conversation = useMessageStore( - useShallow(state => state.conversations.get(conversationId) || null) - ); - - const refresh = useCallback(async () => { - await fetchConversationDetail(conversationId); - }, [conversationId, fetchConversationDetail]); - - return { - conversation, - refresh, - }; -} - -// ==================== useMessageManager - 通用状态 ==================== - -interface UseMessageManagerReturn { - isConnected: boolean; -} - -/** - * 通用消息管理状态 - * 使用 Zustand selector 直接订阅连接状态 - */ -export function useMessageManager(initialize: () => Promise): UseMessageManagerReturn { - // 使用 Zustand selector 直接订阅连接状态 - const isConnected = useMessageStore(state => state.isWSConnected); - - useEffect(() => { - initialize().catch(error => { - console.error('[useMessageManager] 初始化失败:', error); - }); - }, []); - - return { - isConnected, - }; -} - -// ==================== useCreateConversation - 创建会话 ==================== - -interface UseCreateConversationReturn { - createConversation: (userId: string) => Promise; - isCreating: boolean; -} - -/** - * 创建会话 - */ -export function useCreateConversation( - createConversationService: (userId: string) => Promise -): UseCreateConversationReturn { - const [isCreating, setIsCreating] = useState(false); - - const createConversation = useCallback(async (userId: string) => { - setIsCreating(true); - try { - const conversation = await createConversationService(userId); - return conversation; - } catch (error) { - console.error('[useCreateConversation] 创建会话失败:', error); - return null; - } finally { - setIsCreating(false); - } - }, [createConversationService]); - - return { - createConversation, - isCreating, - }; -} - -// ==================== useUpdateConversation - 更新会话 ==================== - -interface UseUpdateConversationReturn { - updateConversation: (conversationId: string, updates: Partial) => void; -} - -/** - * 更新会话 - */ -export function useUpdateConversation( - updateConversationService: (conversationId: string, updates: Partial) => void -): UseUpdateConversationReturn { - const updateConversation = useCallback((conversationId: string, updates: Partial) => { - updateConversationService(conversationId, updates); - }, [updateConversationService]); - - return { - updateConversation, - }; -} - -// ==================== useGroupTyping - 群聊输入状态 ==================== - -interface UseGroupTypingReturn { - typingUsers: string[]; -} - -/** - * 获取群聊输入状态 - * 使用 Zustand selector 直接订阅 - */ -export function useGroupTyping(groupId: string): UseGroupTypingReturn { - // 使用 useShallow 避免每次返回新数组引用导致的无限循环 - const typingUsers = useMessageStore( - useShallow(state => state.typingUsersMap.get(groupId) || []) - ); - - return { - typingUsers, - }; -} - -// ==================== useGroupMuted - 群聊禁言状态 ==================== - -interface UseGroupMutedReturn { - isMuted: boolean; - setMutedStatus: (muted: boolean) => void; -} - -/** - * 获取群聊禁言状态 - * 使用 Zustand selector 直接订阅 - */ -export function useGroupMuted(groupId: string): UseGroupMutedReturn { - // 使用 Zustand selector 直接订阅禁言状态 - const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); - - const setMutedStatus = useCallback((muted: boolean) => { - useMessageStore.getState().setMutedStatus(groupId, muted); - }, [groupId]); - - return { - isMuted, - setMutedStatus, - }; -} - -// ==================== useConnectionStatus - 连接状态 ==================== - -interface UseConnectionStatusReturn { - isConnected: boolean; -} - -/** - * 获取连接状态 - * 使用 Zustand selector 直接订阅 - */ -export function useConnectionStatus(): UseConnectionStatusReturn { - const isConnected = useMessageStore(state => state.isWSConnected); - - return { - isConnected, - }; -} - -// ==================== useIsInitialized - 初始化状态 ==================== - -interface UseIsInitializedReturn { - isInitialized: boolean; -} - -/** - * 获取初始化状态 - * 使用 Zustand selector 直接订阅 - */ -export function useIsInitialized(): UseIsInitializedReturn { - const isInitialized = useMessageStore(state => state.isInitialized); - - return { - isInitialized, - }; -} diff --git a/src/stores/message/hooks.ts b/src/stores/message/hooks.ts index 034ee8d..d949c59 100644 --- a/src/stores/message/hooks.ts +++ b/src/stores/message/hooks.ts @@ -12,6 +12,7 @@ */ import { useState, useEffect, useCallback, useRef } from 'react'; +import { useFocusEffect } from 'expo-router'; import { useShallow } from 'zustand/react/shallow'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; import { messageManager } from './MessageManager'; @@ -96,10 +97,15 @@ interface UseMessagesReturn { /** * 获取指定会话的消息 * 使用 Zustand selector 自动订阅消息变化 + * + * 核心修复: + * 1. useFocusEffect:屏幕重新获得焦点时强制同步消息,解决"需要退出重进才能看到新消息"的问题 + * 2. 清理 isLoadingMessages:组件卸载时清除残留的 loading 锁,防止下次进入被阻断 + * 3. 重入时 forceSync:相同 conversationId 重复进入时强制拉取最新消息 */ export function useMessages(conversationId: string | null): UseMessagesReturn { const normalizedConversationId = conversationId ? String(conversationId) : null; - + // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const messages = useMessageStore( useShallow(state => { @@ -107,30 +113,68 @@ export function useMessages(conversationId: string | null): UseMessagesReturn { return state.messagesMap.get(normalizedConversationId) ?? EMPTY_MESSAGES; }) ); - const isLoadingMessages = useMessageStore(state => + const isLoadingMessages = useMessageStore(state => normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false ); const [hasMore, setHasMore] = useState(true); const loadMoreInFlightRef = useRef(false); + // 追踪上次激活的 conversationId,避免同一会话重复激活时不必要的 forceSync + const lastActivatedIdRef = useRef(null); + // 追踪 useEffect 激活时间,与 useFocusEffect 去重 + const lastActivatedAtRef = useRef(0); useEffect(() => { if (!normalizedConversationId) { return; } + lastActivatedAtRef.current = Date.now(); + + const isReentry = lastActivatedIdRef.current === normalizedConversationId; + // 架构入口:激活会话并完成初始化/同步 - messageManager.activateConversation(normalizedConversationId).catch(error => { + // 重入同一会话时 forceSync,确保拿到最新消息 + messageManager.activateConversation(normalizedConversationId, { forceSync: isReentry }).catch(error => { console.error('[useMessages] 激活会话失败:', error); }); + lastActivatedIdRef.current = normalizedConversationId; + return () => { - // 清理活动会话 + // 清理活动会话 + 清除残留的 loading 锁 if (messageManager.getActiveConversation() === normalizedConversationId) { messageManager.setActiveConversation(null); } + useMessageStore.getState().setLoadingMessages(normalizedConversationId, false); }; }, [normalizedConversationId]); + // 屏幕重新获得焦点时,同步活动会话的最新消息 + useFocusEffect( + useCallback(() => { + if (!normalizedConversationId) return; + + // 如果刚被 useEffect 激活(500ms 内),跳过焦点同步,避免重复 + if (Date.now() - lastActivatedAtRef.current < 500) return; + + // 先清除可能残留的 loading 锁,防止上次退出时未正常清理 + useMessageStore.getState().setLoadingMessages(normalizedConversationId, false); + + // 增量同步最新消息(从当前内存最高 seq 开始拉取) + const currentMessages = useMessageStore.getState().getMessages(normalizedConversationId); + const maxSeq = currentMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0); + if (maxSeq > 0) { + messageManager.fetchMessages(normalizedConversationId, maxSeq).catch(error => { + console.error('[useMessages] 焦点同步消息失败:', error); + }); + } else { + messageManager.fetchMessages(normalizedConversationId).catch(error => { + console.error('[useMessages] 焦点同步消息(冷启动)失败:', error); + }); + } + }, [normalizedConversationId]) + ); + const loadMore = useCallback(async () => { if (!conversationId || !hasMore || loadMoreInFlightRef.current) return; diff --git a/src/stores/message/services/MessageSyncService.ts b/src/stores/message/services/MessageSyncService.ts index d7da4fb..366b651 100644 --- a/src/stores/message/services/MessageSyncService.ts +++ b/src/stores/message/services/MessageSyncService.ts @@ -29,9 +29,15 @@ export class MessageSyncService implements IMessageSyncService { /** 正在加载会话列表下一页 */ private loadingMoreConversations = false; - /** fetchUnreadCount 去重:复用 in-flight promise */ + /** fetchUnreadCount 去重:复用 in-flight promise */ private fetchUnreadCountPromise: Promise | null = null; + // ---- 请求去重:in-flight promise maps ---- + private inflightFetches: Map> = new Map(); + private inflightFetchConversations: Promise | null = null; + private inflightSyncBySeq: Promise | null = null; + private inflightSyncByVersion: Promise | null = null; + constructor( getCurrentUserId: () => string | null, readReceiptManager: ReadReceiptManager, @@ -54,9 +60,22 @@ export class MessageSyncService implements IMessageSyncService { } /** - * 获取会话列表 + * 获取会话列表 — 去重入口 */ async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise { + if (this.inflightFetchConversations) return this.inflightFetchConversations; + this.inflightFetchConversations = this._doFetchConversations(forceRefresh, source); + try { + await this.inflightFetchConversations; + } finally { + this.inflightFetchConversations = null; + } + } + + /** + * 获取会话列表 — 核心实现 + */ + private async _doFetchConversations(forceRefresh: boolean, source: string): Promise { const store = useMessageStore.getState(); if (store.isLoading() && !forceRefresh) { @@ -177,15 +196,29 @@ export class MessageSyncService implements IMessageSyncService { } /** - * 获取会话消息(增量同步) + * 获取会话消息(增量同步)— 去重入口 */ - async fetchMessages(conversationId: string, afterSeq?: number): Promise { - const store = useMessageStore.getState(); + async fetchMessages(conversationId: string, afterSeq?: number, force?: boolean): Promise { + const key = `${conversationId}:${afterSeq ?? 'all'}`; + const existing = this.inflightFetches.get(key); + if (existing) return existing; - // 防止重复加载 - if (store.isLoadingMessages(conversationId)) { - return; + if (!force && useMessageStore.getState().isLoadingMessages(conversationId)) return; + + const promise = this._doFetchMessages(conversationId, afterSeq); + this.inflightFetches.set(key, promise); + try { + await promise; + } finally { + this.inflightFetches.delete(key); } + } + + /** + * 获取会话消息(增量同步)— 核心实现 + */ + private async _doFetchMessages(conversationId: string, afterSeq?: number): Promise { + const store = useMessageStore.getState(); store.setLoadingMessages(conversationId, true); @@ -398,10 +431,22 @@ export class MessageSyncService implements IMessageSyncService { } /** - * 基于 seq 的轻量增量同步(用于重连场景,替代全量刷新) - * 返回 true 表示同步成功,false 表示需要退化为全量刷新 + * 基于 seq 的轻量增量同步 — 去重入口 */ async syncBySeq(): Promise { + if (this.inflightSyncBySeq) return this.inflightSyncBySeq; + this.inflightSyncBySeq = this._doSyncBySeq(); + try { + return await this.inflightSyncBySeq; + } finally { + this.inflightSyncBySeq = null; + } + } + + /** + * 基于 seq 的轻量增量同步 — 核心实现 + */ + private async _doSyncBySeq(): Promise { try { const serverItems = await messageService.getSyncData(); if (!serverItems || serverItems.length === 0) return false; @@ -462,10 +507,22 @@ export class MessageSyncService implements IMessageSyncService { } /** - * 基于版本号的增量同步(替代 syncBySeq,减少带宽) - * 需要本地存储 syncVersion,首次同步时 version=0 会触发 full_sync + * 基于版本号的增量同步 — 去重入口 */ async syncByVersion(): Promise { + if (this.inflightSyncByVersion) return this.inflightSyncByVersion; + this.inflightSyncByVersion = this._doSyncByVersion(); + try { + return await this.inflightSyncByVersion; + } finally { + this.inflightSyncByVersion = null; + } + } + + /** + * 基于版本号的增量同步 — 核心实现 + */ + private async _doSyncByVersion(): Promise { try { const store = useMessageStore.getState(); const version = store.syncVersion ?? 0; diff --git a/src/stores/message/services/WSMessageHandler.ts b/src/stores/message/services/WSMessageHandler.ts index ef59e56..67ec2cc 100644 --- a/src/stores/message/services/WSMessageHandler.ts +++ b/src/stores/message/services/WSMessageHandler.ts @@ -24,7 +24,7 @@ import type { IUserCacheService, HandleNewMessageOptions, } from '../types'; -import { MAX_FLUSH_ITERATIONS } from '../constants'; +import { MAX_FLUSH_ITERATIONS, MIN_RECONNECT_SYNC_INTERVAL } from '../constants'; import { useMessageStore, normalizeConversationId } from '../store'; export class WSMessageHandler implements IWSMessageHandler { @@ -41,6 +41,10 @@ export class WSMessageHandler implements IWSMessageHandler { private isBootstrapping: boolean = false; private lastReconnectSyncAt: number = 0; + // 同步触发器状态:防止 onConnect 和 sync_required 并发 + private syncInProgress: boolean = false; + private lastSyncTriggerAt: number = 0; + // 系统通知去重(防止重连时重复递增系统未读) private processedNotificationIds: Set = new Set(); @@ -166,67 +170,13 @@ export class WSMessageHandler implements IWSMessageHandler { // 监听连接状态 wsService.onConnect(() => { useMessageStore.getState().setSSEConnected(true); - - // 冷启动/重连兜底 - const now = Date.now(); - if (now - this.lastReconnectSyncAt > 1500) { - this.lastReconnectSyncAt = now; - - // 优先尝试 seq 驱动的轻量同步(内部会为 stale 的活动会话拉取消息) - this.syncBySeqCallback().then(ok => { - if (!ok) { - // 增量同步失败或差异过大,退化为全量刷新 - this.fetchConversationsCallback(true, 'sse-reconnect').catch(error => { - console.error('[WSMessageHandler] 连接后同步会话失败:', error); - }); - // 退化场景下仍需同步活动会话消息 - const activeConv = useMessageStore.getState().getActiveConversation(); - if (activeConv) { - this.fetchMessagesCallback(activeConv).catch(() => {}); - } - } - this.fetchUnreadCountCallback().catch(error => { - console.error('[WSMessageHandler] 连接后同步未读数失败:', error); - }); - }).catch(() => { - // syncBySeq 异常,退化为全量 - this.fetchConversationsCallback(true, 'sse-reconnect').catch(() => {}); - const activeConv = useMessageStore.getState().getActiveConversation(); - if (activeConv) { - this.fetchMessagesCallback(activeConv).catch(() => {}); - } - this.fetchUnreadCountCallback().catch(() => {}); - }); - } + this.triggerSync('sse-reconnect'); }); // 监听 sync_required 事件,触发增量同步 wsService.on('sync_required', () => { console.log('[WSMessageHandler] 收到 sync_required,开始增量同步'); - - // 先尝试 seq 驱动的轻量同步(内部会为 stale 的活动会话拉取消息) - this.syncBySeqCallback().then(ok => { - if (!ok) { - this.fetchConversationsCallback(true, 'sync_required').catch(error => { - console.error('[WSMessageHandler] sync_required 同步会话失败:', error); - }); - // 退化场景下仍需同步活动会话消息 - const activeConv = useMessageStore.getState().getActiveConversation(); - if (activeConv) { - this.fetchMessagesCallback(activeConv).catch(() => {}); - } - } - this.fetchUnreadCountCallback().catch(error => { - console.error('[WSMessageHandler] sync_required 同步未读数失败:', error); - }); - }).catch(() => { - this.fetchConversationsCallback(true, 'sync_required').catch(() => {}); - const activeConv = useMessageStore.getState().getActiveConversation(); - if (activeConv) { - this.fetchMessagesCallback(activeConv).catch(() => {}); - } - this.fetchUnreadCountCallback().catch(() => {}); - }); + this.triggerSync('sync_required'); }); wsService.onDisconnect(() => { @@ -259,6 +209,41 @@ export class WSMessageHandler implements IWSMessageHandler { this.isBootstrapping = value; } + /** + * 统一同步入口 — 节流 + 去重 + * onConnect 和 sync_required 共用此入口,防止并发触发重复同步 + */ + private async triggerSync(source: string): Promise { + const now = Date.now(); + // 最小间隔节流 + if (now - this.lastSyncTriggerAt < MIN_RECONNECT_SYNC_INTERVAL) return; + if (this.syncInProgress) return; + + this.syncInProgress = true; + this.lastSyncTriggerAt = now; + + try { + const ok = await this.syncBySeqCallback(); + if (!ok) { + await this.fetchConversationsCallback(true, source); + const activeConv = useMessageStore.getState().getActiveConversation(); + if (activeConv) { + await this.fetchMessagesCallback(activeConv).catch(() => {}); + } + } + await this.fetchUnreadCountCallback().catch(() => {}); + } catch { + await this.fetchConversationsCallback(true, source).catch(() => {}); + const activeConv = useMessageStore.getState().getActiveConversation(); + if (activeConv) { + await this.fetchMessagesCallback(activeConv).catch(() => {}); + } + await this.fetchUnreadCountCallback().catch(() => {}); + } finally { + this.syncInProgress = false; + } + } + /** * 初始化完成后,处理缓冲的 SSE 事件 */ diff --git a/src/stores/message/types.ts b/src/stores/message/types.ts index 992946a..f88d7c7 100644 --- a/src/stores/message/types.ts +++ b/src/stores/message/types.ts @@ -70,7 +70,7 @@ export interface IMessageSyncService { fetchConversations(forceRefresh?: boolean, source?: string): Promise; loadMoreConversations(): Promise; fetchConversationDetail(conversationId: string): Promise; - fetchMessages(conversationId: string, afterSeq?: number): Promise; + fetchMessages(conversationId: string, afterSeq?: number, force?: boolean): Promise; loadMoreMessages(conversationId: string, beforeSeq: number, limit?: number): Promise; fetchUnreadCount(): Promise; syncBySeq(): Promise; diff --git a/src/utils/inputStyles.ts b/src/utils/inputStyles.ts deleted file mode 100644 index 202da48..0000000 --- a/src/utils/inputStyles.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 输入框辅助样式 - * 用于处理React Native Web在浏览器中的默认focus outline样式 - */ - -/** - * 移除浏览器默认focus outline的样式 - * 这个样式可以覆盖浏览器原生的黑框/蓝框 - */ -export const noFocusOutline = { - // 移除浏览器默认的outline(针对Web) - outlineWidth: 0, - outline: 'none' as const, - outlineColor: 'transparent', - // 兼容React Native Web - ':focus': { - outlineWidth: 0, - outline: 'none', - }, - // Webkit浏览器兼容 - '::placeholder': { - outlineWidth: 0, - }, -}; - -/** - * 为TextInput添加无outline样式 - */ -export const inputNoFocusOutline = { - ...noFocusOutline, - // 确保placeholder也没有outline - placeholderTextColor: undefined, // 会在使用时覆盖 -};