diff --git a/package-lock.json b/package-lock.json index 9f66b3a4b..222fb4e7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6067,6 +6067,7 @@ "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz", "integrity": "sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 20.19.4" } @@ -6212,6 +6213,7 @@ "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.4.tgz", "integrity": "sha512-8mpnvfcLcnVh+t1ok6V9eozWo8Ut+TZhz8ylJ6gF9d6q9EGDQX6s8jenan5Yv/pzN4vQEKI4ib2pTf/FELw+SA==", "license": "MIT", + "peer": true, "dependencies": { "@react-native/dev-middleware": "0.81.4", "debug": "^4.4.0", @@ -6242,6 +6244,7 @@ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.4.tgz", "integrity": "sha512-SU05w1wD0nKdQFcuNC9D6De0ITnINCi8MEnx9RsTD2e4wN83ukoC7FpXaPCYyP6+VjFt5tUKDPgP1O7iaNXCqg==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">= 20.19.4" } @@ -6251,6 +6254,7 @@ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.4.tgz", "integrity": "sha512-hu1Wu5R28FT7nHXs2wWXvQ++7W7zq5GPY83llajgPlYKznyPLAY/7bArc5rAzNB7b0kwnlaoPQKlvD/VP9LZug==", "license": "MIT", + "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.4", @@ -6273,6 +6277,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -6289,6 +6294,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", + "peer": true, "dependencies": { "async-limiter": "~1.0.0" } @@ -6354,6 +6360,7 @@ "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.4.tgz", "integrity": "sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 20.19.4" } @@ -6363,6 +6370,7 @@ "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.4.tgz", "integrity": "sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 20.19.4" } @@ -6378,6 +6386,7 @@ "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.4.tgz", "integrity": "sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA==", "license": "MIT", + "peer": true, "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -6880,18 +6889,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@siteed/expo-audio-studio": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/@siteed/expo-audio-studio/-/expo-audio-studio-2.18.1.tgz", - "integrity": "sha512-gyi02iaj/ZEpMOnFM7jlgXWhy4I6pvu7dK7f95nQgFkuSDogmM/pA/n+lS31ui5HzDbVRle426EpQ1hShVmqDA==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "expo-modules-core": "~2.4.0", - "react": "*", - "react-native": "*" - } - }, "node_modules/@speechmatics/expo-two-way-audio": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@speechmatics/expo-two-way-audio/-/expo-two-way-audio-0.1.2.tgz", @@ -12220,19 +12217,6 @@ "node": ">=8.0" } }, - "node_modules/expo-status-bar": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz", - "integrity": "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==", - "license": "MIT", - "dependencies": { - "react-native-is-edge-to-edge": "^1.2.1" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/expo-structured-headers": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-5.0.0.tgz", @@ -17430,6 +17414,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz", "integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.4", @@ -17519,7 +17504,6 @@ "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.7.0.tgz", "integrity": "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17576,12 +17560,11 @@ } }, "node_modules/react-native-nitro-modules": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.31.1.tgz", - "integrity": "sha512-ecr3bTWvLj5/Ce3BO54XRHy4uNHfFR2iVFLf9g/zDRkaD4iOg6kF9hatkIXwUPZi6apIf9P91BS+bco0lSIcVA==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.30.0.tgz", + "integrity": "sha512-DHFSEbM7cwVHmpbGvmd+b5iqy67My0In9oBc8eSMvjTkLWrDkkiE5jLdt6ZJyDF7Fn3C5Tr6Lq2bUa596VSW2g==", "hasInstallScript": true, "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17730,55 +17713,6 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, - "node_modules/react-native-webrtc": { - "version": "124.0.7", - "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz", - "integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==", - "license": "MIT", - "dependencies": { - "base64-js": "1.5.1", - "debug": "4.3.4", - "event-target-shim": "6.0.2" - }, - "peerDependencies": { - "react-native": ">=0.60.0" - } - }, - "node_modules/react-native-webrtc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/react-native-webrtc/node_modules/event-target-shim": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", - "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/react-native-webrtc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/react-native-worklets": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", @@ -17820,6 +17754,7 @@ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.4.tgz", "integrity": "sha512-LWTGUTzFu+qOQnvkzBP52B90Ym3stZT8IFCzzUrppz8Iwglg83FCtDZAR4yLHI29VY/x/+pkcWAMCl3739XHdw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", @@ -17840,13 +17775,15 @@ "version": "0.81.4", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.4.tgz", "integrity": "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-native/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -17857,6 +17794,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -17867,6 +17805,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -17887,6 +17826,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -17899,6 +17839,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", + "peer": true, "dependencies": { "async-limiter": "~1.0.0" } @@ -21175,7 +21116,6 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", - "@siteed/expo-audio-studio": "^2.18.1", "@speechmatics/expo-two-way-audio": "^0.1.2", "buffer": "^6.0.3", "expo": "^54.0.18", @@ -21192,7 +21132,6 @@ "expo-linking": "~8.0.8", "expo-router": "~6.0.13", "expo-splash-screen": "~31.0.10", - "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.7", "expo-updates": "~29.0.12", @@ -21200,10 +21139,12 @@ "lucide-react-native": "^0.546.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-native": "0.81.4", + "react-native": "^0.81.5", "react-native-css": "^3.0.1", + "react-native-edge-to-edge": "^1.7.0", "react-native-gesture-handler": "~2.28.0", "react-native-markdown-display": "^7.0.2", + "react-native-nitro-modules": "^0.30.0", "react-native-permissions": "^5.4.2", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", @@ -21211,7 +21152,6 @@ "react-native-svg": "^15.14.0", "react-native-unistyles": "^3.0.15", "react-native-web": "~0.21.0", - "react-native-webrtc": "^124.0.7", "react-native-worklets": "0.5.1" }, "devDependencies": { @@ -21222,6 +21162,79 @@ "typescript": "~5.9.2" } }, + "packages/app/node_modules/@react-native/assets-registry": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", + "integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "packages/app/node_modules/@react-native/community-cli-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz", + "integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.81.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.83.1", + "metro-config": "^0.83.1", + "metro-core": "^0.83.1", + "semver": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "packages/app/node_modules/@react-native/gradle-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz", + "integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "packages/app/node_modules/@react-native/js-polyfills": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz", + "integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "packages/app/node_modules/@react-native/normalize-colors": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", + "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", + "license": "MIT" + }, + "packages/app/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "packages/app/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -21246,6 +21259,137 @@ "ieee754": "^1.2.1" } }, + "packages/app/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/app/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/app/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "packages/app/node_modules/react-native": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", + "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.81.5", + "@react-native/codegen": "0.81.5", + "@react-native/community-cli-plugin": "0.81.5", + "@react-native/gradle-plugin": "0.81.5", + "@react-native/js-polyfills": "0.81.5", + "@react-native/normalize-colors": "0.81.5", + "@react-native/virtualized-lists": "0.81.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.83.1", + "metro-source-map": "^0.83.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "packages/app/node_modules/react-native/node_modules/@react-native/virtualized-lists": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", + "integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "packages/app/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "packages/server": { "name": "@voice-dev/server", "version": "0.1.0", diff --git a/packages/app/app.json b/packages/app/app.json index f9c6aeb8e..4195d6f5f 100644 --- a/packages/app/app.json +++ b/packages/app/app.json @@ -57,12 +57,6 @@ } ], "expo-audio", - [ - "@siteed/expo-audio-studio", - { - "enableBackgroundAudio": true - } - ], [ "expo-build-properties", { @@ -75,7 +69,7 @@ ], "experiments": { "typedRoutes": true, - "reactCompiler": false + "reactCompiler": true }, "extra": { "router": {}, diff --git a/packages/app/babel.config.js b/packages/app/babel.config.js index 23a1ed4c9..ed101b402 100644 --- a/packages/app/babel.config.js +++ b/packages/app/babel.config.js @@ -2,6 +2,13 @@ module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], - plugins: [["react-native-unistyles/plugin", { root: "app" }]], + plugins: [ + [ + "react-native-unistyles/plugin", + { + root: "src", + }, + ], + ], }; }; diff --git a/packages/app/hooks/use-realtime-audio.ts b/packages/app/hooks/use-realtime-audio.ts deleted file mode 100644 index 56d39a592..000000000 --- a/packages/app/hooks/use-realtime-audio.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import { Platform } from 'react-native'; -import { useAudioRecorder } from '@siteed/expo-audio-studio'; -import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; - -export interface RealtimeAudioConfig { - onAudioSegment?: (audioData: string) => void; - onSpeechStart?: () => void; - onSpeechEnd?: () => void; - onError?: (error: Error) => void; - volumeThreshold?: number; // RMS threshold for speech detection (0-1) - silenceDuration?: number; // ms of silence before ending segment - speechStartDuration?: number; // ms of sustained volume before starting segment -} - -export interface RealtimeAudio { - start: () => Promise; - stop: () => Promise; - isActive: boolean; - isSpeaking: boolean; -} - -/** - * Convert raw PCM data to WAV format by adding WAV headers - * @param pcmBase64 Base64-encoded PCM data - * @param sampleRate Sample rate (default: 16000) - * @param channels Number of channels (default: 1 for mono) - * @param bitsPerSample Bits per sample (default: 16) - * @returns Base64-encoded WAV data - */ -function pcmToWav( - pcmBase64: string, - sampleRate: number = 16000, - channels: number = 1, - bitsPerSample: number = 16 -): string { - // Decode base64 PCM data - const pcmBinary = atob(pcmBase64); - const pcmLength = pcmBinary.length; - - // Calculate WAV parameters - const byteRate = sampleRate * channels * (bitsPerSample / 8); - const blockAlign = channels * (bitsPerSample / 8); - const dataSize = pcmLength; - const fileSize = 44 + dataSize; // WAV header is 44 bytes - - // Create WAV buffer - const wavBuffer = new Uint8Array(fileSize); - const view = new DataView(wavBuffer.buffer); - - // Write WAV header - let offset = 0; - - // RIFF chunk descriptor - writeString(view, offset, 'RIFF'); offset += 4; - view.setUint32(offset, fileSize - 8, true); offset += 4; // File size - 8 - writeString(view, offset, 'WAVE'); offset += 4; - - // fmt sub-chunk - writeString(view, offset, 'fmt '); offset += 4; - view.setUint32(offset, 16, true); offset += 4; // Subchunk size (16 for PCM) - view.setUint16(offset, 1, true); offset += 2; // Audio format (1 = PCM) - view.setUint16(offset, channels, true); offset += 2; - view.setUint32(offset, sampleRate, true); offset += 4; - view.setUint32(offset, byteRate, true); offset += 4; - view.setUint16(offset, blockAlign, true); offset += 2; - view.setUint16(offset, bitsPerSample, true); offset += 2; - - // data sub-chunk - writeString(view, offset, 'data'); offset += 4; - view.setUint32(offset, dataSize, true); offset += 4; - - // Copy PCM data - for (let i = 0; i < pcmLength; i++) { - wavBuffer[offset + i] = pcmBinary.charCodeAt(i); - } - - // Convert to base64 - let binary = ''; - for (let i = 0; i < wavBuffer.length; i++) { - binary += String.fromCharCode(wavBuffer[i]); - } - return btoa(binary); -} - -/** - * Helper to write string to DataView - */ -function writeString(view: DataView, offset: number, str: string): void { - for (let i = 0; i < str.length; i++) { - view.setUint8(offset + i, str.charCodeAt(i)); - } -} - -/** - * Decode base64 string to Uint8Array - */ -function decodeBase64ToUint8Array(base64: string): Uint8Array { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; -} - -/** - * Encode Uint8Array to base64 string - */ -function uint8ArrayToBase64(bytes: Uint8Array): string { - let binary = ''; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -} - -/** - * Concatenate multiple Uint8Arrays into one - */ -function concatenateUint8Arrays(arrays: Uint8Array[]): Uint8Array { - const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - for (const arr of arrays) { - result.set(arr, offset); - offset += arr.length; - } - return result; -} - -/** - * Hook for realtime audio streaming with volume-based VAD - * Uses expo-audio-studio for recording with echo cancellation and noise suppression - */ -export function useRealtimeAudio(config: RealtimeAudioConfig): RealtimeAudio { - const [isActive, setIsActive] = useState(false); - const [isSpeaking, setIsSpeaking] = useState(false); - - const audioRecorder = useAudioRecorder(); - const audioBufferRef = useRef([]); - const silenceStartRef = useRef(null); - const isSpeakingRef = useRef(false); - const speechStartRef = useRef(null); // Track when volume first exceeded threshold - const speechConfirmedRef = useRef(false); // Track if speech has been confirmed - const lastVolumeCheckRef = useRef(Date.now()); - const lastVolumeLogRef = useRef(Date.now()); - - const VOLUME_THRESHOLD = config.volumeThreshold ?? 0.2; - const SILENCE_DURATION_MS = config.silenceDuration ?? 1000; - const SPEECH_START_DURATION_MS = config.speechStartDuration ?? 1000; - const VOLUME_CHECK_INTERVAL = 200; // Check volume every 200ms - - async function requestMicrophonePermission(): Promise { - try { - const permission = Platform.OS === 'ios' - ? PERMISSIONS.IOS.MICROPHONE - : PERMISSIONS.ANDROID.RECORD_AUDIO; - - const status = await check(permission); - if (status === RESULTS.GRANTED) { - return true; - } - - const result = await request(permission); - return result === RESULTS.GRANTED; - } catch (error) { - console.error('[RealtimeAudio] Permission error:', error); - return false; - } - } - - function calculateVolume(base64Audio: string): number { - try { - // Decode base64 to binary - const binary = atob(base64Audio); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - - // Calculate RMS from PCM data (16-bit signed PCM, little-endian) - let sum = 0; - const sampleCount = Math.floor(bytes.length / 2); - - for (let i = 0; i < bytes.length - 1; i += 2) { - // Read 16-bit value as unsigned first - let sample = bytes[i] | (bytes[i + 1] << 8); - - // Convert to signed 16-bit (two's complement) - if (sample > 32767) { - sample -= 65536; - } - - // Normalize to -1.0 to 1.0 range - const normalized = sample / 32768.0; - sum += normalized * normalized; - } - - const rms = Math.sqrt(sum / sampleCount); - return rms; - } catch (error) { - console.error('[RealtimeAudio] Volume calculation error:', error); - return 0; - } - } - - async function start(): Promise { - if (isActive) { - console.log('[RealtimeAudio] Already active'); - return; - } - - try { - console.log('[RealtimeAudio] Starting realtime audio...'); - - // Request permissions - const hasPermission = await requestMicrophonePermission(); - if (!hasPermission) { - throw new Error('Microphone permission denied'); - } - - // Start recording with audio streaming - await audioRecorder.startRecording({ - sampleRate: 16000, - channels: 1, - encoding: 'pcm_16bit', - interval: 100, // Get chunks every 100ms - onAudioStream: async (event: any) => { - if (!event.data) return; - - const audioData = event.data; // base64 PCM data - - // Check volume periodically - const now = Date.now(); - if (now - lastVolumeCheckRef.current >= VOLUME_CHECK_INTERVAL) { - lastVolumeCheckRef.current = now; - - const volume = calculateVolume(audioData); - const speechDetected = volume > VOLUME_THRESHOLD; - - // Decode base64 chunk to binary for buffering - const decodedChunk = decodeBase64ToUint8Array(audioData); - - // Check if we're in detection phase (volume detected but not yet confirmed) - const inDetectionPhase = speechStartRef.current !== null && !isSpeakingRef.current; - - if (inDetectionPhase) { - // We're in detection phase - check timer regardless of current volume - const sustainedDuration = Date.now() - speechStartRef.current!; - - if (sustainedDuration >= SPEECH_START_DURATION_MS) { - // Timer elapsed - confirm speech - console.log('[RealtimeAudio] ✅ Speech confirmed after', sustainedDuration, 'ms (volume:', volume.toFixed(4), 'threshold:', VOLUME_THRESHOLD, ')'); - isSpeakingRef.current = true; - speechConfirmedRef.current = true; - setIsSpeaking(true); - config.onSpeechStart?.(); - audioBufferRef.current.push(decodedChunk); - silenceStartRef.current = null; - } else if (speechDetected) { - // Still in detection phase, volume still above threshold, keep buffering - audioBufferRef.current.push(decodedChunk); - - // Log progress every 500ms during detection - const logNow = Date.now(); - if (logNow - lastVolumeLogRef.current >= 500) { - console.log('[RealtimeAudio] 🔍 Detecting...', sustainedDuration, '/', SPEECH_START_DURATION_MS, 'ms (volume:', volume.toFixed(4), ')'); - lastVolumeLogRef.current = logNow; - } - } else { - // Volume dropped before timer elapsed - false start - console.log('[RealtimeAudio] ❌ False start (volume dropped after', sustainedDuration, 'ms, needed', SPEECH_START_DURATION_MS, 'ms)'); - speechStartRef.current = null; - speechConfirmedRef.current = false; - audioBufferRef.current = []; - } - } else if (speechDetected && !isSpeakingRef.current) { - // First volume detection - start timer and buffering - console.log('[RealtimeAudio] 🎤 Volume spike detected! Starting detection... (volume:', volume.toFixed(4), 'threshold:', VOLUME_THRESHOLD, ')'); - speechStartRef.current = Date.now(); - speechConfirmedRef.current = false; - audioBufferRef.current = [decodedChunk]; - silenceStartRef.current = null; - } else if (speechDetected && isSpeakingRef.current) { - // Continuing confirmed speech - just buffer, no logs - audioBufferRef.current.push(decodedChunk); - silenceStartRef.current = null; - } else if (!speechDetected && isSpeakingRef.current) { - // Potential speech END (after confirmed speech) - audioBufferRef.current.push(decodedChunk); - - if (silenceStartRef.current === null) { - silenceStartRef.current = Date.now(); - console.log('[RealtimeAudio] 🔇 Silence detected, waiting', SILENCE_DURATION_MS, 'ms to confirm end...'); - } else { - const silenceDuration = Date.now() - silenceStartRef.current; - if (silenceDuration >= SILENCE_DURATION_MS) { - // Speech END confirmed - const totalChunks = audioBufferRef.current.length; - console.log('[RealtimeAudio] 🛑 Speech ended after', silenceDuration, 'ms silence (captured', totalChunks, 'chunks)'); - isSpeakingRef.current = false; - speechStartRef.current = null; - speechConfirmedRef.current = false; - silenceStartRef.current = null; - setIsSpeaking(false); - config.onSpeechEnd?.(); - - // Send buffered audio segment - if (audioBufferRef.current.length > 0) { - // Concatenate all binary chunks - const combinedBinary = concatenateUint8Arrays(audioBufferRef.current); - const audioSizeKb = (combinedBinary.length / 1024).toFixed(2); - console.log('[RealtimeAudio] 📤 Sending audio segment:', audioSizeKb, 'KB'); - // Convert to base64 - const combinedPcmBase64 = uint8ArrayToBase64(combinedBinary); - // Convert PCM to WAV format - const wavAudio = pcmToWav(combinedPcmBase64, 16000, 1, 16); - config.onAudioSegment?.(wavAudio); - audioBufferRef.current = []; - } - } - } - } - } else if (isSpeakingRef.current) { - // Just buffer if we're speaking but not checking volume yet - const decodedChunk = decodeBase64ToUint8Array(audioData); - audioBufferRef.current.push(decodedChunk); - } - }, - }); - - setIsActive(true); - console.log('[RealtimeAudio] 🎙️ Audio capture started (threshold:', VOLUME_THRESHOLD, 'duration:', SPEECH_START_DURATION_MS, 'ms)'); - } catch (error) { - console.error('[RealtimeAudio] Start error:', error); - const err = error instanceof Error ? error : new Error(String(error)); - config.onError?.(err); - - // Cleanup on error - await stop(); - throw error; - } - } - - async function stop(): Promise { - console.log('[RealtimeAudio] Stopping realtime audio...'); - - // Stop recording - if (audioRecorder.isRecording) { - try { - await audioRecorder.stopRecording(); - } catch (error) { - console.error('[RealtimeAudio] Error stopping recording:', error); - } - } - - // Reset state - audioBufferRef.current = []; - isSpeakingRef.current = false; - speechStartRef.current = null; - speechConfirmedRef.current = false; - silenceStartRef.current = null; - setIsActive(false); - setIsSpeaking(false); - - console.log('[RealtimeAudio] Realtime audio stopped'); - } - - // Cleanup on unmount - useEffect(() => { - return () => { - if (isActive) { - stop(); - } - }; - }, []); - - return { - start, - stop, - isActive, - isSpeaking, - }; -} diff --git a/packages/app/index.ts b/packages/app/index.ts new file mode 100644 index 000000000..80b707a67 --- /dev/null +++ b/packages/app/index.ts @@ -0,0 +1,2 @@ +import "expo-router/entry"; +import "./src/styles/unistyles"; diff --git a/packages/app/package.json b/packages/app/package.json index 2fce63cf7..e1d7cf50e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@voice-dev/app", - "main": "expo-router/entry", + "main": "index.ts", "version": "1.0.0", "scripts": { "start": "expo start", @@ -16,7 +16,6 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", - "@siteed/expo-audio-studio": "^2.18.1", "@speechmatics/expo-two-way-audio": "^0.1.2", "buffer": "^6.0.3", "expo": "^54.0.18", @@ -33,7 +32,6 @@ "expo-linking": "~8.0.8", "expo-router": "~6.0.13", "expo-splash-screen": "~31.0.10", - "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.7", "expo-updates": "~29.0.12", @@ -41,10 +39,12 @@ "lucide-react-native": "^0.546.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-native": "0.81.4", + "react-native": "^0.81.5", "react-native-css": "^3.0.1", + "react-native-edge-to-edge": "^1.7.0", "react-native-gesture-handler": "~2.28.0", "react-native-markdown-display": "^7.0.2", + "react-native-nitro-modules": "^0.30.0", "react-native-permissions": "^5.4.2", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", @@ -52,7 +52,6 @@ "react-native-svg": "^15.14.0", "react-native-unistyles": "^3.0.15", "react-native-web": "~0.21.0", - "react-native-webrtc": "^124.0.7", "react-native-worklets": "0.5.1" }, "devDependencies": { @@ -61,10 +60,5 @@ "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "typescript": "~5.9.2" - }, - "overrides": { - "@siteed/expo-audio-studio": { - "expo-modules-core": "3.0.22" - } } } diff --git a/packages/app/app/_layout.tsx b/packages/app/src/app/_layout.tsx similarity index 78% rename from packages/app/app/_layout.tsx rename to packages/app/src/app/_layout.tsx index 5e85d062d..014f1b70f 100644 --- a/packages/app/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -1,6 +1,4 @@ -import '@/styles/unistyles'; import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; import { SafeAreaProvider } from 'react-native-safe-area-context'; export default function RootLayout() { @@ -11,7 +9,6 @@ export default function RootLayout() { - ); } diff --git a/packages/app/app/audio-test.tsx b/packages/app/src/app/audio-test.tsx similarity index 100% rename from packages/app/app/audio-test.tsx rename to packages/app/src/app/audio-test.tsx diff --git a/packages/app/app/index.tsx b/packages/app/src/app/index.tsx similarity index 99% rename from packages/app/app/index.tsx rename to packages/app/src/app/index.tsx index e0c3f13d7..15222c215 100644 --- a/packages/app/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -13,7 +13,7 @@ import { router } from "expo-router"; import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet } from "react-native-unistyles"; -import { theme as defaultTheme } from "@/styles/theme"; +import { theme as defaultTheme } from "../styles/theme"; // Simple unique ID generator let messageIdCounter = 0; @@ -1126,7 +1126,10 @@ export default function VoiceAssistantScreen() { !ws.isConnected && styles.buttonDisabled, isRecording && styles.mainButtonRecording, isInProgress && styles.mainButtonInProgress, - userInput.trim() && !isRecording && !isInProgress && styles.mainButtonWithText, + userInput.trim() && + !isRecording && + !isInProgress && + styles.mainButtonWithText, ]} > {isInProgress ? ( diff --git a/packages/app/app/settings.tsx b/packages/app/src/app/settings.tsx similarity index 100% rename from packages/app/app/settings.tsx rename to packages/app/src/app/settings.tsx diff --git a/packages/app/components/active-processes.tsx b/packages/app/src/components/active-processes.tsx similarity index 100% rename from packages/app/components/active-processes.tsx rename to packages/app/src/components/active-processes.tsx diff --git a/packages/app/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx similarity index 100% rename from packages/app/components/agent-stream-view.tsx rename to packages/app/src/components/agent-stream-view.tsx diff --git a/packages/app/components/artifact-drawer.tsx b/packages/app/src/components/artifact-drawer.tsx similarity index 100% rename from packages/app/components/artifact-drawer.tsx rename to packages/app/src/components/artifact-drawer.tsx diff --git a/packages/app/components/connection-status.tsx b/packages/app/src/components/connection-status.tsx similarity index 100% rename from packages/app/components/connection-status.tsx rename to packages/app/src/components/connection-status.tsx diff --git a/packages/app/components/conversation-selector.tsx b/packages/app/src/components/conversation-selector.tsx similarity index 100% rename from packages/app/components/conversation-selector.tsx rename to packages/app/src/components/conversation-selector.tsx diff --git a/packages/app/components/message.tsx b/packages/app/src/components/message.tsx similarity index 100% rename from packages/app/components/message.tsx rename to packages/app/src/components/message.tsx diff --git a/packages/app/components/voice-button.tsx b/packages/app/src/components/voice-button.tsx similarity index 100% rename from packages/app/components/voice-button.tsx rename to packages/app/src/components/voice-button.tsx diff --git a/packages/app/constants/theme.ts b/packages/app/src/constants/theme.ts similarity index 100% rename from packages/app/constants/theme.ts rename to packages/app/src/constants/theme.ts diff --git a/packages/app/hooks/use-audio-player.ts b/packages/app/src/hooks/use-audio-player.ts similarity index 100% rename from packages/app/hooks/use-audio-player.ts rename to packages/app/src/hooks/use-audio-player.ts diff --git a/packages/app/hooks/use-audio-recorder.ts b/packages/app/src/hooks/use-audio-recorder.ts similarity index 100% rename from packages/app/hooks/use-audio-recorder.ts rename to packages/app/src/hooks/use-audio-recorder.ts diff --git a/packages/app/hooks/use-color-scheme.ts b/packages/app/src/hooks/use-color-scheme.ts similarity index 100% rename from packages/app/hooks/use-color-scheme.ts rename to packages/app/src/hooks/use-color-scheme.ts diff --git a/packages/app/hooks/use-color-scheme.web.ts b/packages/app/src/hooks/use-color-scheme.web.ts similarity index 100% rename from packages/app/hooks/use-color-scheme.web.ts rename to packages/app/src/hooks/use-color-scheme.web.ts diff --git a/packages/app/hooks/use-settings.ts b/packages/app/src/hooks/use-settings.ts similarity index 100% rename from packages/app/hooks/use-settings.ts rename to packages/app/src/hooks/use-settings.ts diff --git a/packages/app/hooks/use-speechmatics-audio.ts b/packages/app/src/hooks/use-speechmatics-audio.ts similarity index 100% rename from packages/app/hooks/use-speechmatics-audio.ts rename to packages/app/src/hooks/use-speechmatics-audio.ts diff --git a/packages/app/hooks/use-theme-color.ts b/packages/app/src/hooks/use-theme-color.ts similarity index 100% rename from packages/app/hooks/use-theme-color.ts rename to packages/app/src/hooks/use-theme-color.ts diff --git a/packages/app/hooks/use-websocket.ts b/packages/app/src/hooks/use-websocket.ts similarity index 100% rename from packages/app/hooks/use-websocket.ts rename to packages/app/src/hooks/use-websocket.ts diff --git a/packages/app/styles/theme.ts b/packages/app/src/styles/theme.ts similarity index 100% rename from packages/app/styles/theme.ts rename to packages/app/src/styles/theme.ts diff --git a/packages/app/src/styles/unistyles.ts b/packages/app/src/styles/unistyles.ts new file mode 100644 index 000000000..648fe73d6 --- /dev/null +++ b/packages/app/src/styles/unistyles.ts @@ -0,0 +1,48 @@ +import { StyleSheet } from "react-native-unistyles"; +// import { UnistylesRuntime } from "react-native-unistyles"; +import { lightTheme, darkTheme } from "./theme"; + +console.log("[Unistyles] Configuring..."); + +// // Configure Unistyles with adaptive themes +StyleSheet.configure({ + themes: { + light: lightTheme, + dark: darkTheme, + }, + breakpoints: { + xs: 0, + sm: 576, + md: 768, + lg: 992, + xl: 1200, + }, + settings: { + initialTheme: "dark", + }, +}); + +console.log("[Unistyles] Configuration complete!"); + +// Type augmentation for TypeScript +type AppThemes = { + light: typeof lightTheme; + dark: typeof darkTheme; +}; + +type AppBreakpoints = { + xs: number; + sm: number; + md: number; + lg: number; + xl: number; +}; + +declare module "react-native-unistyles" { + export interface UnistylesThemes extends AppThemes {} + export interface UnistylesBreakpoints extends AppBreakpoints {} +} + +console.log(lightTheme.colors.background); + +// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background); diff --git a/packages/app/styles/unistyles.ts b/packages/app/styles/unistyles.ts deleted file mode 100644 index b652ce754..000000000 --- a/packages/app/styles/unistyles.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StyleSheet } from "react-native-unistyles"; -import { lightTheme, darkTheme } from "./theme"; - -// Configure Unistyles with adaptive themes -StyleSheet.configure({ - themes: { - light: lightTheme, - dark: darkTheme, - }, - settings: { - adaptiveThemes: true, - }, -}); - -// Type augmentation for TypeScript -type AppThemes = { - light: typeof lightTheme; - dark: typeof darkTheme; -}; - -declare module "react-native-unistyles" { - export interface UnistylesThemes extends AppThemes {} -} diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 594b62bfb..2bef2745c 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -3,12 +3,8 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ], - "@server/*": [ - "../server/src/*" - ] + "@/*": ["./src/*"], + "@server/*": ["../server/src/*"] } }, "include": [ @@ -18,4 +14,4 @@ "expo-env.d.ts", "nativewind-env.d.ts" ] -} \ No newline at end of file +} diff --git a/packages/app/unistyles.md b/packages/app/unistyles.md new file mode 100644 index 000000000..e33bdfd0f --- /dev/null +++ b/packages/app/unistyles.md @@ -0,0 +1,8616 @@ +This is the full developer documentation for React Native Unistyles 3.0 + +# Configuration + +> How configure Unistyles + +To unlock more features and tailor Unistyles to your needs, you can configure it. The Unistyles configuration is divided into three parts: + +1. **Themes** +2. **Breakpoints** +3. **Settings** + +Note + +Each configuration is optional but enables advanced features, which are explained in the guide [How Unistyles Works?](/v3/start/how-unistyles-works) + +### Themes (Optional) + +`Themes` is a JavaScript object where the keys represent unique theme names, and the values are the corresponding theme definitions. For more details, refer to the [theming](/v3/guides/theming) guide. + +unistyles.ts + +```tsx +const lightTheme = { + colors: { + primary: '#ff1ff4', + secondary: '#1ff4ff' + // any nesting, spreading, arrays, etc. + }, + // functions, external imports, etc. + gap: (v: number) => v * 8 +} + + +const otherTheme = { + colors: { + primary: '#aa12ff', + secondary: 'pink' + }, + gap: (v: number) => v * 8 +} + + +const appThemes = { + light: lightTheme, + other: otherTheme +} +``` + +Note + +Unistyles supports any dynamic theme and doesn’t enforce a specific structure. To avoid TypeScript issues, ensure that all themes share the same type. + +### Breakpoints (Optional) + +`Breakpoints` is a JavaScript object where the keys are unique breakpoint names and the values are the corresponding breakpoint values (numbers). Be sure to register at least one breakpoint with a value of 0, as it’s required to simulate the cascading behavior of CSS media queries. + +unistyles.ts + +```tsx +const breakpoints = { + xs: 0, // <-- make sure to register one breakpoint with value 0 + sm: 300, + md: 500, + lg: 800, + xl: 1200 + // use as many breakpoints as you need +} +``` + +### Settings (Optional) + +The `Settings` object has been simplified, and in the most recent version, it supports only four properties: + +* **`adaptiveThemes`** – a boolean that enables or disables adaptive themes [learn more](/v3/guides/theming#adaptive-themes) +* **`initialTheme`** – a string or a synchronous function that sets the initial theme +* **`CSSVars`** – a boolean that enables or disables web CSS variables (defaults to `true`) [learn more](/v3/references/web-only#css-variables) +* **`nativeBreakpointsMode`** - iOS/Android only. User preferred mode for breakpoints. Can be either `points` or `pixels` (defaults to `pixels`) [learn more](/v3/references/breakpoints#pixelpoint-mode-for-native-breakpoints) + +unistyles.ts + +```tsx +const settings = { + initialTheme: 'light' +} + + +// or with a synchronous function +const settings = { + initialTheme: () => { + // get preferred theme from user's preferences/MMKV/SQL/StanJS etc. + + + return storage.getString('preferredTheme') ?? 'light' + } +} + + +// or with adaptive themes +const settings = { + adaptiveThemes: true +} +``` + +Note + +In the Unistyles 3.0 setting both `initialTheme` and `adaptiveThemes` will cause an error. These options are mutually exclusive. + +### TypeScript Types (Optional) + +If your repository is using TypeScript, it is highly recommended to override the library types for optimal autocomplete and type safety regarding your themes and breakpoints: + +unistyles.ts + +```tsx +type AppThemes = typeof appThemes +type AppBreakpoints = typeof breakpoints + + +declare module 'react-native-unistyles' { + export interface UnistylesThemes extends AppThemes {} + export interface UnistylesBreakpoints extends AppBreakpoints {} +} +``` + +### Set configuration + +The final step in the configuration is to set all the options by calling the `StyleSheet.configure` function: + +unistyles.ts + +```tsx +import { StyleSheet } from 'react-native-unistyles' + + +StyleSheet.configure({ + themes: appThemes, + breakpoints, + settings +}) +``` + +That’s it! You can now use all the features of Unistyles in your project! + +Note + +Don’t forget to import this config somewhere in your project, for example in `index.ts` file. You **must** call `StyleSheet.configure` **before** any `StyleSheet.create` call. + +For expo router users, please refer to the [Expo Router guide](/v3/guides/expo-router). + +### Full example + +unistyles.ts + +```tsx +import { StyleSheet } from 'react-native-unistyles' + + +const lightTheme = { + colors: { + primary: '#ff1ff4', + secondary: '#1ff4ff' + }, + gap: (v: number) => v * 8 +} + + +const otherTheme = { + colors: { + primary: '#aa12ff', + secondary: 'pink' + }, + gap: (v: number) => v * 8 +} + + +const appThemes = { + light: lightTheme, + other: otherTheme +} + + +const breakpoints = { + xs: 0, + sm: 300, + md: 500, + lg: 800, + xl: 1200 +} + + +type AppBreakpoints = typeof breakpoints +type AppThemes = typeof appThemes + + +declare module 'react-native-unistyles' { + export interface UnistylesThemes extends AppThemes {} + export interface UnistylesBreakpoints extends AppBreakpoints {} +} + + +StyleSheet.configure({ + settings: { + initialTheme: 'light', + }, + breakpoints, + themes: appThemes +}) +``` + +# Getting started + +> How to get started with Unistyles + +We’ve made Unistyles incredibly easy to use. You no longer need the `useStyle` hook or wrap your app in React Provider. Unistyles integrates seamlessly with your existing code, so you can start using it immediately. + +### Prerequisites + +Unistyles 3.0 is tightly integrated with `Fabric` and the latest versions of React Native. Therefore, you must use the **New Architecture** and at least **React Native 0.78.0**. Additionally, Unistyles relies on `react-native-nitro-modules` and `react-native-edge-to-edge`. + +Note + +Learn more about how Unistyles leverages Nitro Modules and React Native Edge to Edge [here](/v3/other/dependencies). + +**Table of requirements:** + +| | Required | Note | +| ---------------- | ------------------------- | ------------------------- | +| React Native | 0.78.0+ | | +| New Architecture | enabled | no option to opt-out | +| Expo SDK | 53+ | (if you use Expo) | +| Xcode | 16+ (recommended 16.3+) | Required by Nitro Modules | +| Platform | iOS / Android / Web / SSR | Follow instructions below | + +Since Unistyles relies on `Fabric`, it cannot run on the `Old Architecture` or older versions of React Native. If you can’t meet these requirements, you can use Unistyles 2.0+, which is compatible with those versions. + +### Installation + +Install Unistyles and its dependencies + +```shell +yarn add react-native-unistyles react-native-nitro-modules react-native-edge-to-edge +``` + +Caution + +To avoid unexpected behaviors always use a fixed version of `react-native-nitro-modules`. Check compatibility table [here](https://github.com/jpudysz/react-native-unistyles?tab=readme-ov-file#installation). + +Add babel plugin: + +babel.config.js + +```js +module.exports = function (api) { + api.cache(true) + + + return { + // for bare React Native + // presets: ['module:@react-native/babel-preset'], + + + // or for Expo + // presets: ['babel-preset-expo'], + + + // other config + plugins: [ + // other plugins + ['react-native-unistyles/plugin', { + // pass root folder of your application + // all files under this folder will be processed by the Babel plugin + // if you need to include more folders, or customize discovery process + // check available babel options + root: 'src' + }] + ] + } +} +``` + +Note + +See additional Babel plugin configuration options [here](/v3/other/babel-plugin#extra-configuration). + +Learn why you need Babel plugin [here](/v3/other/babel-plugin). + +Finish installation based on your platform: + +* Expo + + ```shell + yarn expo prebuild --clean + ``` + + Do you use Expo Router? + + Finish installation for Expo Router [here](/v3/guides/expo-router). + + Dev client only + + Unistyles includes custom native code, which means it does not support **Expo Go.** + +* React Native + + ```shell + cd ios && pod install + ``` + +* React Native Web + + Unistyles offers first-class support for React Native Web. To run the project, we recommend following the guidelines provided by [Expo](https://docs.expo.dev/workflow/web/). + +* Custom Web + + You can use Unistyles without React Native Web as a dependency. Check [this guide](/v3/guides/custom-web) for more details. + +* SSR + + Unistyles offers first-class support for Next.js Server Side Rendering. To run the project, we recommend following the guidelines provided by [Next.JS](https://nextjs.org/docs). + + Then follow [SSR guide](/v3/guides/server-side-rendering). + + Babel only + + You need to disable SWC and rely on Babel for transpiling your code. + +### As easy as React Native StyleSheet + +Getting started with Unistyles couldn’t be easier. Simply replace React Native’s `StyleSheet` with the `StyleSheet` exported from Unistyles. From that moment, you’ll be using a `StyleSheet` with superpowers 🦸🏼‍♂️. + +Example.tsx + +```tsx + import { StyleSheet } from 'react-native' + import { StyleSheet } from 'react-native-unistyles' + + +const MyComponent = () => { + return ( + + Hello world from Unistyles + + ) +} + + +const styles = StyleSheet.create({ + container: { + backgroundColor: 'red' + } +}) +``` + +By replacing `StyleSheet`, you immediately gain several benefits that aren’t available in React Native’s `StyleSheet`: + +* [Variants](/v3/references/variants) +* [Compound variants](/v3/references/compound-variants) +* [Dynamic functions](/v3/references/dynamic-functions) +* [Media queries](/v3/references/media-queries) +* [Horizontal and vertical breakpoints for Native](/v3/references/breakpoints#built-in-breakpoints-landscape-and-portrait) +* [Custom web styles](/v3/references/web-styles) +* [Web only features](/v3/references/web-only) + +When you’re ready to customize your styles and unlock additional features you can [configure](/v3/start/configuration) Unistyles. + +# How Unistyles works? + +> Understanding how Unistyles 3.0 works + +To get the most out of Unistyles, it’s important to understand how it works and how it updates your styles. + +### 1. StyleSheets + +A typical app consists of many `StyleSheets`. A `StyleSheet` is a JavaScript object that holds one or many styles. Each style is associated with a native view. What’s more important is that each `StyleSheet` is unique, tailored to the needs of the view, or to a shared component. + +![](/_astro/how-1.ClhanbTT.png) + +Your app’s StyleSheets + +### 2. Babel plugin: dependencies + +Unistyles needs to understand your `StyleSheet` dependencies in order to update them only when necessary. This process begins when Babel transforms your app’s code. At this stage, the Unistyles Babel plugin scans your `StyleSheets` and determines the dependencies for each style: + +```ts +const styles = StyleSheet.create((theme, rt) => ({ + // static: no dependencies + container: { + backgroundColor: 'red', + }, + // depends on theme and font scale + text: { + color: theme.colors.text, + fontSize: rt.fontScale * 16 + }, + dynamic: (isOdd: boolean) => ({ + // depends on theme + color: isOdd ? theme.colors.primary : theme.colors.secondary, + }) +}) +``` + +### 3. Babel plugin: component factory + +As you already know, Unistyles has no components. This means your native view hierarchy remains exactly the same as in your original code. The Babel plugin processes your components through our component factory to borrow `refs` and bind the `ShadowNode` with `Unistyle`. + +You might be wondering, what is `Unistyle`? We refer to it as your `StyleSheet` style that has been parsed by the Unistyles compiler, and with the attached `C++` state. + +![](/_astro/how-2.CKBxY89P.png) + +Your styles are transformed into Unistyles + +Note + +Learn more on how the Babel plugin works [here](/v3/other/babel-plugin). + +### 4. StyleSheet registry + +We don’t just extract metadata from your styles. We do the same for your `StyleSheet`. On the C++ side, we know exactly which `StyleSheet` is static, which depends on a `theme`, and which `Unistyles` it contains. At this point, your app’s `StyleSheets` are reconstructed on the C++ side and stored in native C++ `StyleSheets`, which contain the parsed `Unistyles`. + +![](/_astro/how-3.C8JEACBb.png) + +C++ StyleSheets that contain parsed styles (Unistyles) + +To make this process easier to visualize, imagine that the Unistyles engine is a production line. It takes your raw `StyleSheets`, parses them, and produces their C++ representation with `Unistyles`: + +![](/_astro/how-4.DbcKjxwq.png) + +Unistyles workflow + +### 5. Reacting to events + +When you access your `StyleSheet` styles in your component, you’ll get a regular JS object as expected. If your component re-renders, we simply return the same `Unistyle` that’s already parsed and stored in the cache. + +To visualize the true power of `Unistyles`, imagine that some event occurs, such as: + +* A theme change triggered by the user clicking a button +* A phone color scheme change +* A phone orientation change +* Accessibility settings being updated +* and much more! Unistyles can update your styles based on 16 different events + +At this point, the Unistyles algorithm scans the `StyleSheetRegistry` and looks for styles that depend on this event: + +![](/_astro/how-5.Bu7A8bBJ.png) + +Finding affected styles + +Affected styles are then re-computed to reflect the new state of your app. + +### 6. Shadow Tree updates + +With the list of affected styles, we can now browse the `ShadowRegistry`, where we keep the bindings between `ShadowNode` and `Unistyles`. In other words, we know which `component` relies on which `style`. With all this information, we can translate the update into atomic `ShadowTree` instructions. + +With Unistyles 2.0 or any other library, we would need to re-render your entire app to reflect the changes: + +![](/_astro/how-6.D5izBS6T.png) + +Regular flow: your app is re-rendered + +Instead, with all the optimizations and features that Unistyles 3.0 brings, we can target only specific nodes and update your `ShadowTree` directly from C++: + +![](/_astro/how-7.DHJpQ7un.png) + +Unistyles 3.0 updates only selected ShadowNodes from C++ + +With this architecture and the power of selective updates through `ShadowTree`, your components are never re-rendered. + +*Engineering is the closest thing to magic that exists in the world.* + +\~Elon Musk + +# Introduction + +> Welcome to Unistyles! + +![](/_astro/uni2.CWBtkH-A.png) + +Unistyles is a cross-platform library that enables you to share up to 100% of your styles across all platforms. It combines the simplicity of `StyleSheet` with the performance of `C++`. + +**`Unistyles` is a superset of `StyleSheet`** similar to how `TypeScript` is a superset of `JavaScript`. If you’re familiar with styling in React Native, then you already know how to use `Unistyles`. + +### Why should you use Unistyles? + +* Guarantees no re-renders across the entire app (no hooks, no context—just pure JSI bindings) +* Doesn’t pollute your native view hierarchy, you can use any component you want +* Includes a cross-platform parser written in C++, ensuring consistent output across all platforms +* Leverages [Nitro Modules](https://nitro.margelo.com/) under the hood (everything is strongly typed!) +* Transforms your `StyleSheets` into enhanced `StyleSheets` with superpowers 🦸🏼‍♂️ that can access themes, platform-specific values, and more! +* Loved by developers worldwide: 2M+ downloads and over 2.2K stars on GitHub +* Backed by [@jpudysz](https://github.com/jpudysz) since 2023 + +# Migration guide + +> How to migrate from previous version + +The migration process is quite simple, but it can be tedious since you’ll need to remove a lot of the existing code. + +1. Follow installation steps from [Getting started](/v3/start/getting-started) guide. + +2. Replace your configuration with [new](/v3/start/configuration) one. + + `UnistylesRegistry` can be easily replaced with `StyleSheet.configure` as it follows the same syntax. `Themes` and `Breakpoints` work exactly the same. For `Settings` we removed 4 out of 6 options: + + ```tsx + import { UnistylesRegistry } from 'react-native-unistyles' + import { StyleSheet } from 'react-native-unistyles' + + + UnistylesRegistry.addConfig({ + adaptiveThemes: false, + initialTheme: 'dark', + plugins: [...], + experimentalCSSMediaQueries: true, + windowResizeDebounceTimeMs: 100, + disableAnimatedInsets: true + }) + + + StyleSheet.configure({ + settings: { + adaptiveThemes: false, // works exactly the same like in 2.0 + initialTheme: 'dark', // works exactly the same like in 2.0 + // plugins are removed, instead transform your styles with static functions + // experimentalCSSMediaQueries: these options is also removed, and enabled by default with custom parser + // windowResizeDebounceTimeMs: removed, there is no debouncing anymore. Styles are updated with CSS media queries + // disableAnimatedInsets: removed, insets won't re-render your views + } + }) + ``` + +3. Import `StyleSheet` from `react-native-unistyles`: + + ```tsx + import { createStyleSheet, useStyles } from 'react-native-unistyles' + import { StyleSheet } from 'react-native-unistyles' + ``` + +4. Replace `createStyleSheet` with `StyleSheet.create`: + + ```tsx + const stylesheet = createStyleSheet(theme => ({ + const stylesheet = StyleSheet.create(theme => ({ + ``` + +5. Remove all occurrences of `useStyles` hook: + + ```tsx + const { styles } = useStyles(stylesheet) + ``` + +6. Rename your `stylesheet` to `styles`: + + ```tsx + const stylesheet = StyleSheet.create(theme => ({ + const styles = StyleSheet.create(theme => ({ + ``` + +7. If you used `useInitialTheme`, remove it and set initial theme in `StyleSheet.configure`: + + ```tsx + import { StyleSheet } from 'react-native-unistyles' + + + StyleSheet.configure({ + themes, + breakpoints, + settings: { + initialTheme: () => { + // get preferred theme from user's preferences/MMKV/SQL/StanJS etc. + // must be synchronous + return storage.getString('preferredTheme') ?? 'light' + } + } + }) + ``` + +8. If you need to access your `theme` in component, refactor it to use `withUnistyles`: + + ```tsx + import { Button } from 'react-native' + import { useStyles } from 'react-native-unistyles' + import { withUnistyles } from 'react-native-unistyles' + + + const UniButton = withUnistyles(Button, theme => ({ + color: theme.colors.primary + })) + + + const MyButton = () => { + return + } + + + const MyButton = () => { + const { theme } = useStyles(stylesheet) + + + return