diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fea66f732 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + # By default, a workflow only runs when a pull_request's activity type is + # opened, synchronize, or reopened. Adding ready_for_review here ensures + # that CI runs when a PR is marked as not a draft, since we skip CI when a + # PR is draft + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + all: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Install deps + run: npm ci + - name: Check formatting + run: npm run lint:prettier + + backend: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + defaults: + run: + working-directory: './src/backend' + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Install deps + run: | + npm ci --ignore-scripts + npm run postinstall + - name: Check types + run: npm run lint:types + + frontend: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Install deps + run: npm ci + - name: Build translations + run: npm run build:translations + - name: Build Intl polyfills + run: npm run build:intl-polyfills + - name: Run unit tests + run: npm test diff --git a/README.md b/README.md index 8e801a4f6..e96222b76 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,14 @@ The next version of Mapeo Mobile npm install ``` - 2. (optional) Start the Metro bundler process + 2. Start the Metro bundler process ```sh npm start ``` + Leave this running in a separate terminal window. + 3. Build and run the app (Android) - For this project, using a physical device is more convenient and reliable to work with. It is recommended that you follow React Native's [setup instructions](https://reactnative.dev/docs/running-on-device) for running an app on a device. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d987c6364..6a2e525a5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,12 @@ + + + + + + diff --git a/app.json b/app.json index d9463baab..1d42af137 100644 --- a/app.json +++ b/app.json @@ -1,4 +1,14 @@ { "name": "CoMapeo", - "plugins": ["expo-localization", "expo-secure-store"] + "plugins": [ + "expo-localization", + "expo-secure-store", + [ + "expo-location", + { + "isIosBackgroundLocationEnabled": true, + "isAndroidBackgroundLocationEnabled": true + } + ] + ] } diff --git a/babel.config.js b/babel.config.js index 3c181ebfc..63f23328d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,7 @@ module.exports = { presets: ['module:@react-native/babel-preset'], plugins: [ + 'transform-inline-environment-variables', // react-native-reanimated/plugin has to be last 'react-native-reanimated/plugin', ], diff --git a/messages/en.json b/messages/en.json index 8c9e85e20..f242b7401 100644 --- a/messages/en.json +++ b/messages/en.json @@ -19,6 +19,27 @@ "description": "Title of dialog that shows when cancelling a new observation", "message": "Discard observation?" }, + "Modal.GPSDisable.button": { + "message": "Enable" + }, + "Modal.GPSDisable.description": { + "message": "To create a Track CoMapeo needs access to your location and GPS." + }, + "Modal.GPSDisable.title": { + "message": "GPS Disabled" + }, + "Modal.GPSEnable.button.default": { + "message": "Start Tracks" + }, + "Modal.GPSEnable.button.loading": { + "message": "Loading..." + }, + "Modal.GPSEnable.button.stop": { + "message": "Stop Tracks" + }, + "Modal.GPSEnable.trackingDescription": { + "message": "You’ve been recording for" + }, "Screens.Settings.AppSettings.coordinateSystem": { "message": "Coordinate System" }, @@ -564,6 +585,21 @@ "screens.Settings.CreateOrJoinProject.whatIsAProject": { "message": "What is a Project" }, + "screens.Settings.YourTeam.InviteDeclined": { + "message": "Invitation Declined" + }, + "screens.Settings.YourTeam.close": { + "message": "Close" + }, + "screens.Settings.YourTeam.deviceHasJoined": { + "message": "Device Has Joined {projectName}" + }, + "screens.Settings.YourTeam.inviteDeclinedDes": { + "message": "This device has declined your invitation. They have not joined the project." + }, + "screens.Settings.YourTeam.unableToCancel": { + "message": "Unable to Cancel Invitation" + }, "screens.Settings.aboutMapeo": { "description": "Primary text for 'About Mapeo' link (version info)", "message": "About Mapeo" @@ -596,6 +632,57 @@ "description": "Title of settings screen", "message": "Settings" }, + "screens.Sync.CreateOrJoinProjectDisplay.buttonText": { + "message": "Create or Join Project" + }, + "screens.Sync.CreateOrJoinProjectDisplay.description": { + "message": "Create or Join a Project to sync with other devices" + }, + "screens.Sync.HeaderTitle.noWiFi": { + "message": "No WiFi" + }, + "screens.Sync.NoWifiDisplay.buttonText": { + "message": "Open Settings" + }, + "screens.Sync.NoWifiDisplay.description": { + "message": "Open your phone settings and connect to a WiFi network to synchronize" + }, + "screens.Sync.NoWifiDisplay.title": { + "message": "No WiFi" + }, + "screens.Sync.ProjectSyncDisplay.buttonTextDone": { + "message": "You're all caught up" + }, + "screens.Sync.ProjectSyncDisplay.buttonTextStop": { + "message": "Stop" + }, + "screens.Sync.ProjectSyncDisplay.buttonTextSync": { + "message": "Sync" + }, + "screens.Sync.ProjectSyncDisplay.deviceName": { + "message": "Your device name is {name}" + }, + "screens.Sync.ProjectSyncDisplay.devicesNearby": { + "message": "{count} {count, plural, one {device} other {devices}} nearby/connected" + }, + "screens.Sync.ProjectSyncDisplay.devicesSyncing": { + "message": "Syncing with {count} {count, plural, one {Device} other {Devices}}" + }, + "screens.Sync.ProjectSyncDisplay.devicesWaitingToSync": { + "message": "{count} {count, plural, one {Device} other {Devices}} Waiting to Sync with you" + }, + "screens.Sync.ProjectSyncDisplay.noDevicesSyncing": { + "message": "No Devices are Syncing" + }, + "screens.Sync.ProjectSyncDisplay.syncProgress": { + "message": "{value}%" + }, + "screens.Sync.ProjectSyncDisplay.syncing": { + "message": "Syncing…" + }, + "screens.Sync.ProjectSyncDisplay.upToDate": { + "message": "Up to Date! No data to Sync" + }, "sharedComponents.DeviceCard.ThisDevice": { "message": "This Device!" }, @@ -626,6 +713,9 @@ "sharedComponents.ProjectInviteBottomSheet.goToMap": { "message": "Go To Map" }, + "sharedComponents.ProjectInviteBottomSheet.goToSync": { + "message": "Go To Sync" + }, "sharedComponents.ProjectInviteBottomSheet.invitedToJoin": { "message": "You've been invited to join {projName}" }, diff --git a/package-lock.json b/package-lock.json index d877d02be..6ffdaf1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", "@gorhom/bottom-sheet": "^4.5.1", - "@mapeo/ipc": "^0.2.0", + "@mapeo/ipc": "0.3.0", + "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", "@react-native-picker/picker": "2.6.1", @@ -25,6 +26,7 @@ "@react-navigation/native-stack": "^6.9.13", "@rnmapbox/maps": "^10.1.16", "@tanstack/react-query": "^5.12.2", + "@types/luxon": "^3.4.2", "assert": "^2.0.0", "buffer": "^6.0.3", "cheap-ruler": "^3.0.2", @@ -33,10 +35,14 @@ "expo-camera": "~14.0.5", "expo-crypto": "~12.8.1", "expo-localization": "~14.8.3", - "expo-location": "~16.5.4", + "expo-location": "~16.5.5", "expo-secure-store": "~12.8.1", "expo-sensors": "~12.9.1", + "expo-task-manager": "~11.7.2", + "geojson": "^0.5.0", + "geojson-geometries-lookup": "^0.5.0", "lodash.isequal": "^4.5.0", + "luxon": "^3.4.4", "nanoid": "^5.0.1", "nodejs-mobile-react-native": "^18.17.7", "react": "18.2.0", @@ -75,8 +81,8 @@ "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@formatjs/cli": "^6.2.0", - "@mapeo/core": "^9.0.0-alpha.6", - "@mapeo/schema": "3.0.0-next.13", + "@mapeo/core": "9.0.0-alpha.7", + "@mapeo/schema": "3.0.0-next.15", "@react-native-community/cli": "^12.3.6", "@react-native/babel-preset": "^0.73.21", "@react-native/eslint-config": "^0.73.2", @@ -84,6 +90,7 @@ "@react-native/typescript-config": "^0.74.0", "@testing-library/react-native": "^12.4.3", "@types/debug": "^4.1.7", + "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.12", "@types/lodash.isequal": "^4.5.6", "@types/node": "^20.8.4", @@ -96,6 +103,7 @@ "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-jest": "^29.6.3", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", "eslint": "^8.19.0", "eslint-config-prettier": "^9.1.0", "execa": "^8.0.1", @@ -294,67 +302,17 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.22.9", "license": "MIT", @@ -408,12 +366,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.10", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dependencies": { - "@babel/types": "^7.22.10", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -421,12 +380,13 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -478,16 +438,16 @@ "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.0.tgz", - "integrity": "sha512-QAH+vfvts51BCsNZ2PhY6HAggnlS6omLLFTsIpeqZk/MmJ6cW7tgz5yRv0fMJThcr6FmbMrENh1RgrWPTYA76g==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", + "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -632,12 +592,12 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -726,13 +686,14 @@ } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -789,9 +750,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -858,13 +819,13 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.0.tgz", - "integrity": "sha512-LiT1RqZWeij7X+wGxCoYh3/3b8nVOX6/7BZ9wiQgAIyjoeQWdROaodJCgT+dwtbjHaz0r7bEbHJzjSbVfcOyjQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", + "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.1", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-decorators": "^7.24.0" + "@babel/plugin-syntax-decorators": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -1022,9 +983,9 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.0.tgz", - "integrity": "sha512-MXW3pQCu9gUiVGzqkGqsgiINDVYXoAnrY8FYF/rmb+OfufNF0zHMpHPN4ulRrinxYT8Vk/aZJxYqOKsDECjKAw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", + "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" }, @@ -1813,11 +1774,11 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", - "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1885,12 +1846,12 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", - "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2249,16 +2210,16 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", - "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -2363,18 +2324,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.11", - "license": "MIT", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", - "debug": "^4.1.0", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2650,23 +2612,23 @@ } }, "node_modules/@expo/cli": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.6.tgz", - "integrity": "sha512-vpwQOyhkqQ5Ao96AGaFntRf6dX7h7/e9T7oKZ5KfJiaLRgfmNa/yHFu5cpXG76T2R7Q6aiU4ik0KU3P7nFMzEw==", + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.10.tgz", + "integrity": "sha512-Jw2wY+lsavP9GRqwwLqF/SvB7w2GZ4sWBMcBKTZ8F0lWjwmLGAUt4WYquf20agdmnY/oZUHvWNkrz/t3SflhnA==", "dependencies": { "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~8.5.0", - "@expo/config-plugins": "~7.8.0", + "@expo/config-plugins": "~7.9.0", "@expo/devcert": "^1.0.0", - "@expo/env": "~0.2.0", + "@expo/env": "~0.2.2", "@expo/image-utils": "^0.4.0", "@expo/json-file": "^8.2.37", "@expo/metro-config": "~0.17.0", "@expo/osascript": "^2.0.31", "@expo/package-manager": "^1.1.1", "@expo/plist": "^0.1.0", - "@expo/prebuild-config": "6.7.4", + "@expo/prebuild-config": "6.8.1", "@expo/rudder-sdk-node": "1.1.1", "@expo/spawn-async": "1.5.0", "@expo/xcpretty": "^4.3.0", @@ -3070,12 +3032,12 @@ } }, "node_modules/@expo/config": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-8.5.4.tgz", - "integrity": "sha512-ggOLJPHGzJSJHVBC1LzwXwR6qUn8Mw7hkc5zEKRIdhFRuIQ6s2FE4eOvP87LrNfDF7eZGa6tJQYsiHSmZKG+8Q==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-8.5.6.tgz", + "integrity": "sha512-wF5awSg6MNn1cb1lIgjnhOn5ov2TEUTnkAVCsOl0QqDwcP+YIerteSFwjn9V52UZvg58L+LKxpCuGbw5IHavbg==", "dependencies": { "@babel/code-frame": "~7.10.4", - "@expo/config-plugins": "~7.8.2", + "@expo/config-plugins": "~7.9.0", "@expo/config-types": "^50.0.0", "@expo/json-file": "^8.2.37", "getenv": "^1.0.0", @@ -3088,9 +3050,9 @@ } }, "node_modules/@expo/config-plugins": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-7.8.4.tgz", - "integrity": "sha512-hv03HYxb/5kX8Gxv/BTI8TLc9L06WzqAfHRRXdbar4zkLcP2oTzvsLEF4/L/TIpD3rsnYa0KU42d0gWRxzPCJg==", + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-7.9.1.tgz", + "integrity": "sha512-ICt6Jed1J0tPYMQrJ8K5Qusgih2I6pZ2PU4VSvxsN3T4n97L13XpYV1vyq1Uc/HMl3UhOwldipmgpEbCfeDqsQ==", "dependencies": { "@expo/config-types": "^50.0.0-alpha.1", "@expo/fingerprint": "^0.6.0", @@ -3320,14 +3282,14 @@ "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==" }, "node_modules/@expo/env": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.2.1.tgz", - "integrity": "sha512-deZmRS7Dvp18VM8s559dq/ZjPlV1D9vtLoLXwHmCK/JYOvtNptdKsfxcWjI7ewmo6ln2PqgNI9HRI74q6Wk2eA==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.2.3.tgz", + "integrity": "sha512-a+uJ/e6MAVxPVVN/HbXU5qxzdqrqDwNQYxCfxtAufgmd5VZj54e5f3TJA3LEEUW3pTSZR8xK0H0EtVN297AZnw==", "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", - "dotenv": "~16.0.3", - "dotenv-expand": "~10.0.0", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, @@ -3406,9 +3368,9 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.4.1.tgz", - "integrity": "sha512-EZb+VHSmw+a5s2hS9qksTcWylY0FDaIAVufcxoaRS9tHIXLjW5zcKW7Rhj9dSEbZbRVy9yXXdHKa3GQdUQIOFw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.4.2.tgz", + "integrity": "sha512-CxP+1QXgRXsNnmv2FAUA2RWwK6kNBFg4QEmVXn2K9iLoEAI+i+1IQXcUgc+J7nTJl9pO7FIu2gIiEYGYffjLWQ==", "dependencies": { "@expo/spawn-async": "1.5.0", "chalk": "^4.0.0", @@ -3551,16 +3513,16 @@ } }, "node_modules/@expo/metro-config": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.5.tgz", - "integrity": "sha512-2YUebeIwr6gFxcIRSVAjWK5D8YSaXBzQoRRl3muJWsH8AC8a+T60xbA3cGhsEICD2zKS5zwnL2yobgs41Ur7nQ==", + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.7.tgz", + "integrity": "sha512-3vAdinAjMeRwdhGWWLX6PziZdAPvnyJ6KVYqnJErHHqH0cA6dgAENT3Vq6PEM1H2HgczKr2d5yG9AMgwy848ow==", "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~8.5.0", - "@expo/env": "~0.2.0", + "@expo/env": "~0.2.2", "@expo/json-file": "~8.3.0", "@expo/spawn-async": "^1.7.2", "babel-preset-fbjs": "^3.4.0", @@ -3912,12 +3874,12 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-6.7.4.tgz", - "integrity": "sha512-x8EUdCa8DTMZ/dtEXjHAdlP+ljf6oSeSKNzhycXiHhpMSMG9jEhV28ocCwc6cKsjK5GziweEiHwvrj6+vsBlhA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-6.8.1.tgz", + "integrity": "sha512-ptK9e0dcj1eYlAWV+fG+QkuAWcLAT1AmtEbj++tn7ZjEj8+LkXRM73LCOEGaF0Er8i8ZWNnaVsgGW4vjgP5ZsA==", "dependencies": { "@expo/config": "~8.5.0", - "@expo/config-plugins": "~7.8.0", + "@expo/config-plugins": "~7.9.0", "@expo/config-types": "^50.0.0-alpha.1", "@expo/image-utils": "^0.4.0", "@expo/json-file": "^8.2.37", @@ -4408,10 +4370,7 @@ "node_modules/@formatjs/intl-getcanonicallocales": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz", - "integrity": "sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==", - "dependencies": { - "tslib": "^2.4.0" - } + "integrity": "sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==" }, "node_modules/@formatjs/intl-listformat": { "version": "7.4.0", @@ -5286,8 +5245,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "license": "MIT", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -5319,8 +5279,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "license": "MIT", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5360,9 +5321,9 @@ } }, "node_modules/@mapeo/core": { - "version": "9.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.6.tgz", - "integrity": "sha512-b1eHOmgVbtE02YAMR/kgfgHX5Mt//4gV290pgC+dPpZqccAgDIdBlaviJKRIGHCM16ZETdilx1NvSH+PJHYYGA==", + "version": "9.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.7.tgz", + "integrity": "sha512-8DXZPKtMMVLTgZX3F8Eew3KnLB6bmf9v7uda9TvMX14YM9np4G9IiYafPS/X7kFNvr7h823wMzkRotNnHZ3byg==", "hasInstallScript": true, "dependencies": { "@digidem/types": "^2.2.0", @@ -5372,7 +5333,7 @@ "@fastify/type-provider-typebox": "^3.3.0", "@hyperswarm/secret-stream": "^6.1.2", "@mapeo/crypto": "1.0.0-alpha.10", - "@mapeo/schema": "3.0.0-next.14", + "@mapeo/schema": "3.0.0-next.15", "@mapeo/sqlite-indexer": "1.0.0-alpha.8", "@sinclair/typebox": "^0.29.6", "b4a": "^1.6.3", @@ -5382,7 +5343,7 @@ "compact-encoding": "^2.12.0", "corestore": "^6.8.4", "debug": "^4.3.4", - "drizzle-orm": "0.28.2", + "drizzle-orm": "^0.30.8", "fastify": ">= 4", "fastify-plugin": "^4.5.0", "hyperblobs": "2.3.0", @@ -5407,75 +5368,15 @@ "throttle-debounce": "^5.0.0", "tiny-typed-emitter": "^2.1.0", "type-fest": "^4.5.0", - "undici": "^6.7.0", + "undici": "^6.13.0", "varint": "^6.0.0", "yauzl-promise": "^4.0.0" } }, - "node_modules/@mapeo/core/node_modules/@mapeo/schema": { - "version": "3.0.0-next.14", - "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.14.tgz", - "integrity": "sha512-i0AUHbwMxUyggk6SDURxLPXOVCWlLAszSKUYm2fviQXYrGNcUALniw8JBn3x5jzfCFW1xrTUNhIcnt4IuF95mA==", - "dependencies": { - "@json-schema-tools/dereferencer": "^1.6.1", - "ajv": "^8.12.0", - "ajv-formats": "^2.1.1", - "compact-encoding": "^2.12.0", - "glob": "^10.3.3", - "protobufjs": "^7.2.5", - "type-fest": "^4.1.0", - "typedoc": "^0.24.8", - "typedoc-plugin-markdown": "^3.15.4" - } - }, "node_modules/@mapeo/core/node_modules/@sinclair/typebox": { "version": "0.29.6", "license": "MIT" }, - "node_modules/@mapeo/core/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@mapeo/core/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@mapeo/core/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@mapeo/core/node_modules/hypercore": { "version": "10.17.0", "license": "MIT", @@ -5500,11 +5401,6 @@ "z32": "^1.0.0" } }, - "node_modules/@mapeo/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/@mapeo/core/node_modules/mime": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", @@ -5519,20 +5415,6 @@ "node": ">=16" } }, - "node_modules/@mapeo/core/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@mapeo/core/node_modules/p-timeout": { "version": "6.1.2", "license": "MIT", @@ -5543,50 +5425,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@mapeo/core/node_modules/typedoc": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", - "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", - "dependencies": { - "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.0", - "shiki": "^0.14.1" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 14.14" - }, - "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" - } - }, - "node_modules/@mapeo/core/node_modules/typedoc-plugin-markdown": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz", - "integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==", - "dependencies": { - "handlebars": "^4.7.7" - }, - "peerDependencies": { - "typedoc": ">=0.24.0" - } - }, - "node_modules/@mapeo/core/node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@mapeo/core/node_modules/varint": { "version": "6.0.0", "license": "MIT" @@ -5609,9 +5447,9 @@ } }, "node_modules/@mapeo/ipc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.2.0.tgz", - "integrity": "sha512-qAkKlwpIJw0Srr9pR4w9pbluIADzkMHpRbOuxSYY5+fonmNuEc8esRXWwvaL4SeBEIoUsuIOpWuEDOIoYlRk6Q==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.3.0.tgz", + "integrity": "sha512-8OdARTEBgCFCXrcJqwPuz397i10MiCIGG1Y9SSZ7myzN2o72lbuVAu7SX/D2gbHrVVD2tuia9EwaDBVsznas2w==", "dependencies": { "eventemitter3": "^5.0.1", "p-defer": "^4.0.0", @@ -5621,13 +5459,13 @@ "node": ">=18.17.1" }, "peerDependencies": { - "@mapeo/core": "9.0.0-alpha.6" + "@mapeo/core": "9.0.0-alpha.7" } }, "node_modules/@mapeo/schema": { - "version": "3.0.0-next.13", - "dev": true, - "license": "MIT", + "version": "3.0.0-next.15", + "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.15.tgz", + "integrity": "sha512-7g5GLkDhZLeStPIkVkTqElidng9cKtzNDJSQww5+Z7PWpkniMLDJYLki9TchEJEmPoAgwxgULNBiC69cMnNo9Q==", "dependencies": { "@json-schema-tools/dereferencer": "^1.6.1", "ajv": "^8.12.0", @@ -5642,7 +5480,6 @@ }, "node_modules/@mapeo/schema/node_modules/ajv": { "version": "8.12.0", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5657,7 +5494,6 @@ }, "node_modules/@mapeo/schema/node_modules/brace-expansion": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5665,7 +5501,6 @@ }, "node_modules/@mapeo/schema/node_modules/glob": { "version": "10.3.9", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -5686,12 +5521,10 @@ }, "node_modules/@mapeo/schema/node_modules/json-schema-traverse": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/@mapeo/schema/node_modules/minimatch": { "version": "9.0.3", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -5705,7 +5538,6 @@ }, "node_modules/@mapeo/schema/node_modules/typedoc": { "version": "0.24.8", - "dev": true, "license": "Apache-2.0", "dependencies": { "lunr": "^2.3.9", @@ -5727,7 +5559,6 @@ "version": "3.17.1", "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz", "integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==", - "dev": true, "dependencies": { "handlebars": "^4.7.7" }, @@ -5739,7 +5570,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, "peer": true, "bin": { "tsc": "bin/tsc", @@ -6278,6 +6108,11 @@ "node": ">=14" } }, + "node_modules/@osm_borders/maritime_10000m": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@osm_borders/maritime_10000m/-/maritime_10000m-1.1.0.tgz", + "integrity": "sha512-0gbQoi3ITsqVGMXU5hm6s0TN8MHqA2xQSZ6GeIHZcCuf5f462iP1fMV4JZHNIretwqq9BN9F+4r3hjIHsj7RJw==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -8055,8 +7890,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "license": "BSD-3-Clause" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", @@ -8548,6 +8384,45 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@turf/boolean-contains": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz", + "integrity": "sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ==", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz", + "integrity": "sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@turf/destination": { "version": "6.5.0", "license": "MIT", @@ -8720,8 +8595,9 @@ } }, "node_modules/@types/geojson": { - "version": "7946.0.11", - "license": "MIT" + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, "node_modules/@types/graceful-fs": { "version": "4.1.6", @@ -8813,6 +8689,11 @@ "@types/lodash": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/ms": { "version": "0.7.31", "dev": true, @@ -8959,9 +8840,10 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.8", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9125,9 +9007,10 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.3.8", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9529,6 +9412,21 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-includes": { "version": "3.1.5", "dev": true, @@ -9580,6 +9478,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asap": { "version": "2.0.6", "license": "MIT" @@ -9663,8 +9582,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -9793,6 +9716,12 @@ "@babel/plugin-syntax-flow": "^7.12.1" } }, + "node_modules/babel-plugin-transform-inline-environment-variables": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-0.4.4.tgz", + "integrity": "sha512-bJILBtn5a11SmtR2j/3mBOjX4K3weC6cq+NNZ7hG22wCAqpc3qtj/iN7dSe9HDiS46lgp1nHsQgeYrea/RUe+g==", + "dev": true + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "dev": true, @@ -9816,9 +9745,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-10.0.1.tgz", - "integrity": "sha512-uWIGmLfbP3dS5+8nesxaW6mQs41d4iP7X82ZwRdisB/wAhKQmuJM9Y1jQe4006uNYkw6Phf2TT03ykLVro7KuQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-10.0.2.tgz", + "integrity": "sha512-hg06qdSTK7MjKmFXSiq6cFoIbI3n3uT8a3NI2EZoISWhu+tedCj4DQduwi+3adFuRuYvAwECI0IYn/5iGh5zWQ==", "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", @@ -10308,11 +10237,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11151,6 +11087,54 @@ "resolved": "https://registry.npmjs.org/dag-map/-/dag-map-1.0.2.tgz", "integrity": "sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==" }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dayjs": { "version": "1.11.6", "license": "MIT" @@ -11367,6 +11351,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -11376,9 +11376,11 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "license": "MIT", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -11641,42 +11643,58 @@ } }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "dependencies": { + "dotenv": "^16.4.4" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/drizzle-orm": { - "version": "0.28.2", - "license": "Apache-2.0", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.30.8.tgz", + "integrity": "sha512-9pBJA0IjnpPpzZ6s9jlS1CQAbKoBmbn2GJesPhXaVblAA/joOJ4AWWevYcqvLGj9SvThBAl7WscN8Zwgg5mnTw==", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", "@libsql/client": "*", "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@types/better-sqlite3": "*", "@types/pg": "*", + "@types/react": ">=18", "@types/sql.js": "*", - "@vercel/postgres": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", + "expo-sqlite": ">=13.2.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", + "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, @@ -11687,12 +11705,18 @@ "@cloudflare/workers-types": { "optional": true }, + "@electric-sql/pglite": { + "optional": true + }, "@libsql/client": { "optional": true }, "@neondatabase/serverless": { "optional": true }, + "@op-engineering/op-sqlite": { + "optional": true + }, "@opentelemetry/api": { "optional": true }, @@ -11705,18 +11729,27 @@ "@types/pg": { "optional": true }, + "@types/react": { + "optional": true + }, "@types/sql.js": { "optional": true }, "@vercel/postgres": { "optional": true }, + "@xata.io/client": { + "optional": true + }, "better-sqlite3": { "optional": true }, "bun-types": { "optional": true }, + "expo-sqlite": { + "optional": true + }, "knex": { "optional": true }, @@ -11732,6 +11765,9 @@ "postgres": { "optional": true }, + "react": { + "optional": true + }, "sql.js": { "optional": true }, @@ -11902,34 +11938,56 @@ } }, "node_modules/es-abstract": { - "version": "1.20.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -11938,6 +11996,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "dev": true, @@ -11948,7 +12049,6 @@ }, "node_modules/es-to-primitive": { "version": "1.2.1", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.4", @@ -12599,23 +12699,23 @@ } }, "node_modules/expo": { - "version": "50.0.8", - "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.8.tgz", - "integrity": "sha512-8yXsoMbFRjWyEDNuFRtH0vTFvEjFnnwP+LceS6xmXGp+IW1hKdN1X6Bj1EUocFtepH0ruHDPCof1KvPoWfUWkw==", + "version": "50.0.17", + "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.17.tgz", + "integrity": "sha512-eD8Nh10BgVwecU7EVyogx7X314ajxVpJdFwkXhi341AD61S2WPX31NMHW82XGXas6dbDjdbgtaOMo5H/vylB7Q==", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "0.17.6", - "@expo/config": "8.5.4", - "@expo/config-plugins": "7.8.4", - "@expo/metro-config": "0.17.5", + "@expo/cli": "0.17.10", + "@expo/config": "8.5.6", + "@expo/config-plugins": "7.9.1", + "@expo/metro-config": "0.17.7", "@expo/vector-icons": "^14.0.0", - "babel-preset-expo": "~10.0.1", + "babel-preset-expo": "~10.0.2", "expo-asset": "~9.0.2", - "expo-file-system": "~16.0.7", + "expo-file-system": "~16.0.9", "expo-font": "~11.10.3", "expo-keep-awake": "~12.8.2", "expo-modules-autolinking": "1.10.3", - "expo-modules-core": "1.11.9", + "expo-modules-core": "1.11.13", "fbemitter": "^3.0.0", "whatwg-url-without-unicode": "8.0.0-3" }, @@ -12670,9 +12770,9 @@ } }, "node_modules/expo-file-system": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz", - "integrity": "sha512-BELr1Agj6WK0PKVMcD0rqC3fP5unKfp2KW8/sNhtTHgdzQ/F0Pylq9pTk9u7KEu0ZbEdTpk5EMarLMPwffi3og==", + "version": "16.0.9", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.9.tgz", + "integrity": "sha512-3gRPvKVv7/Y7AdD9eHMIdfg5YbUn2zbwKofjsloTI5sEC57SLUFJtbLvUCz9Pk63DaSQ7WIE1JM0EASyvuPbuw==", "peerDependencies": { "expo": "*" } @@ -12708,9 +12808,9 @@ } }, "node_modules/expo-location": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-16.5.4.tgz", - "integrity": "sha512-jJ675jhL5N3azaWIu2v585ivERIBMoBBKYMrAbxrACsAmU2qMpEkQMcJ1pAFClUZhNBsONoa06NTr2/JZbGDMw==", + "version": "16.5.5", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-16.5.5.tgz", + "integrity": "sha512-dXEd1HaZgdi6yHVF8R+SMnGlKDYrD+Hkkzd/b9edjMSUBLxF2y824AFSSNUf6BVOM53tJBOFEELneXkU1uj9nA==", "peerDependencies": { "expo": "*" } @@ -12830,9 +12930,9 @@ } }, "node_modules/expo-modules-core": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.9.tgz", - "integrity": "sha512-GTUb81vcPaF+5MtlBI1u9IjrZbGdF1ZUwz3u8Gc+rOLBblkZ7pYsj2mU/tu+k0khTckI9vcH4ZBksXWvE1ncjQ==", + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.13.tgz", + "integrity": "sha512-2H5qrGUvmLzmJNPDOnovH1Pfk5H/S/V0BifBmOQyDc9aUh9LaDwkqnChZGIXv8ZHDW8JRlUW0QqyWxTggkbw1A==", "dependencies": { "invariant": "^2.2.4" } @@ -12856,6 +12956,17 @@ "expo": "*" } }, + "node_modules/expo-task-manager": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-11.7.2.tgz", + "integrity": "sha512-cmn7xg8+mGP7gX6deYZhvrCkKMkoBRJ+E4o5aL17Z/4ihXMfo/PFcQsrpuSYRLXzgidEw0kpppxhmYm21Jswwg==", + "dependencies": { + "unimodules-app-loader": "~4.5.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "license": "Apache-2.0" @@ -13467,14 +13578,14 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "dev": true, - "license": "MIT", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -13485,7 +13596,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13498,6 +13608,36 @@ "node": ">=6.9.0" } }, + "node_modules/geojson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz", + "integrity": "sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/geojson-geometries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/geojson-geometries/-/geojson-geometries-2.0.0.tgz", + "integrity": "sha512-HKljxKnbJrdkr7ijg5/nvcr1b81HP+C/rS48cJwRb3xEqiEynlGkRVkQ/msopw+EE3gf7DjEKdZyEGMck0bOpw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/geojson-geometries-lookup": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson-geometries-lookup/-/geojson-geometries-lookup-0.5.0.tgz", + "integrity": "sha512-AfadxaBda6VTwwX4USLiVofFaz0HIjubC7ZC15hGTvc8K0di3pmCNWGFotKJeURSxYPbJLFoet34X3JrIKFX4A==", + "dependencies": { + "@turf/bbox": "^6.0.1", + "@turf/boolean-contains": "^6.0.1", + "geojson-geometries": "^2.0.0", + "rbush": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/geojson-rbush": { "version": "3.2.0", "license": "MIT", @@ -13521,12 +13661,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "license": "MIT", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13667,12 +13813,13 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -13852,6 +13999,7 @@ }, "node_modules/has": { "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -13862,7 +14010,6 @@ }, "node_modules/has-bigints": { "version": "1.0.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13876,10 +14023,22 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13896,10 +14055,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -13909,9 +14069,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -14321,12 +14481,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.3", - "dev": true, - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -14421,13 +14581,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "license": "MIT" }, "node_modules/is-bigint": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -14438,7 +14612,6 @@ }, "node_modules/is-boolean-object": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -14477,9 +14650,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -14624,9 +14810,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "engines": { "node": ">= 0.4" }, @@ -14636,7 +14822,6 @@ }, "node_modules/is-number-object": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -14683,8 +14868,8 @@ }, "node_modules/is-regex": { "version": "1.1.4", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -14697,11 +14882,14 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14720,7 +14908,6 @@ }, "node_modules/is-string": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -14734,7 +14921,6 @@ }, "node_modules/is-symbol": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -14747,10 +14933,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "license": "MIT", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -14782,7 +14969,6 @@ }, "node_modules/is-weakref": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2" @@ -15193,20 +15379,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/jest-cli/node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-cli/node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -17283,6 +17455,14 @@ "version": "2.3.9", "license": "MIT" }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-bytes.js": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", @@ -18808,9 +18988,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "dev": true, - "license": "MIT", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -18837,12 +19017,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "dev": true, - "license": "MIT", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -19549,10 +19729,18 @@ "node": ">=4.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -19570,7 +19758,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -19997,8 +20185,9 @@ } }, "node_modules/react-devtools-core": { - "version": "4.28.0", - "license": "MIT", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", + "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -21059,17 +21248,6 @@ "node": ">=12" } }, - "node_modules/react-native/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/react-native/node_modules/scheduler": { "version": "0.24.0-canary-efb381bbf-20230505", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", @@ -21094,17 +21272,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/react-native/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/react-native/node_modules/ws": { "version": "6.2.2", "license": "MIT", @@ -21282,13 +21449,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "dev": true, - "license": "MIT", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -21608,6 +21776,28 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "license": "MIT" @@ -21619,14 +21809,17 @@ "optional": true }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -21764,6 +21957,36 @@ "version": "2.6.0", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "license": "MIT" @@ -21953,7 +22176,6 @@ }, "node_modules/side-channel": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -22242,8 +22464,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -22490,27 +22713,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { - "version": "1.0.5", - "dev": true, - "license": "MIT", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "dev": true, - "license": "MIT", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22751,8 +22994,9 @@ "license": "CC0-1.0" }, "node_modules/tar": { - "version": "6.2.0", - "license": "ISC", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -23133,9 +23377,14 @@ } }, "node_modules/traverse": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", - "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz", + "integrity": "sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==", + "dependencies": { + "gopd": "^1.0.1", + "typedarray.prototype.slice": "^1.0.3", + "which-typed-array": "^1.1.15" + }, "engines": { "node": ">= 0.4" }, @@ -23217,6 +23466,94 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz", + "integrity": "sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-errors": "^1.3.0", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-offset": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -23283,7 +23620,6 @@ }, "node_modules/unbox-primitive": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -23296,9 +23632,9 @@ } }, "node_modules/undici": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.10.1.tgz", - "integrity": "sha512-kSzmWrOx3XBKTgPm4Tal8Hyl3yf+hzlA00SAf4goxv8LZYafKmS6gJD/7Fe5HH/DMNiFTRXvkwhLo7mUn5fuQQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.13.0.tgz", + "integrity": "sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==", "engines": { "node": ">=18.0" } @@ -23339,6 +23675,11 @@ "node": ">=4" } }, + "node_modules/unimodules-app-loader": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-4.5.0.tgz", + "integrity": "sha512-q/Xug4K6/20876Xac+tjOLOOAeHEu2zF66LNN/5c8EV4WPEe/+RYZEljN/woQt17KPIB2eyel9dc+d6qUMjUOg==" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -23759,7 +24100,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -23777,14 +24117,15 @@ "license": "ISC" }, "node_modules/which-typed-array": { - "version": "1.1.11", - "license": "MIT", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index a81acffd2..1ca04dd4f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test": "jest", "lint:prettier": "prettier \"src/**/*.{js,ts,jsx,tsx}\" --check", "lint:eslint": "eslint . --ext .js,.jsx,.ts,.tsx --cache --ignore-path .gitignore", - "lint": "npm run lint:prettier && npm run lint:eslint", + "lint:types": "tsc --noEmit", + "lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:types", "postinstall": "patch-package", "prepare": "husky install", "format": "prettier \"src/**/*.{js,ts,jsx,tsx}\" --write", @@ -29,7 +30,8 @@ "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", "@gorhom/bottom-sheet": "^4.5.1", - "@mapeo/ipc": "^0.2.0", + "@mapeo/ipc": "0.3.0", + "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", "@react-native-picker/picker": "2.6.1", @@ -38,6 +40,7 @@ "@react-navigation/native-stack": "^6.9.13", "@rnmapbox/maps": "^10.1.16", "@tanstack/react-query": "^5.12.2", + "@types/luxon": "^3.4.2", "assert": "^2.0.0", "buffer": "^6.0.3", "cheap-ruler": "^3.0.2", @@ -46,10 +49,14 @@ "expo-camera": "~14.0.5", "expo-crypto": "~12.8.1", "expo-localization": "~14.8.3", - "expo-location": "~16.5.4", + "expo-location": "~16.5.5", "expo-secure-store": "~12.8.1", "expo-sensors": "~12.9.1", + "expo-task-manager": "~11.7.2", + "geojson": "^0.5.0", + "geojson-geometries-lookup": "^0.5.0", "lodash.isequal": "^4.5.0", + "luxon": "^3.4.4", "nanoid": "^5.0.1", "nodejs-mobile-react-native": "^18.17.7", "react": "18.2.0", @@ -88,8 +95,8 @@ "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@formatjs/cli": "^6.2.0", - "@mapeo/core": "^9.0.0-alpha.6", - "@mapeo/schema": "3.0.0-next.13", + "@mapeo/core": "9.0.0-alpha.7", + "@mapeo/schema": "3.0.0-next.15", "@react-native-community/cli": "^12.3.6", "@react-native/babel-preset": "^0.73.21", "@react-native/eslint-config": "^0.73.2", @@ -97,6 +104,7 @@ "@react-native/typescript-config": "^0.74.0", "@testing-library/react-native": "^12.4.3", "@types/debug": "^4.1.7", + "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.12", "@types/lodash.isequal": "^4.5.6", "@types/node": "^20.8.4", @@ -109,6 +117,7 @@ "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-jest": "^29.6.3", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", "eslint": "^8.19.0", "eslint-config-prettier": "^9.1.0", "execa": "^8.0.1", diff --git a/src/backend/package-lock.json b/src/backend/package-lock.json index bd7ce4ef4..7404de6c8 100644 --- a/src/backend/package-lock.json +++ b/src/backend/package-lock.json @@ -10,8 +10,8 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@mapeo/core": "^9.0.0-alpha.6", - "@mapeo/ipc": "^0.2.0", + "@mapeo/core": "9.0.0-alpha.7", + "@mapeo/ipc": "0.3.0", "debug": "^4.3.4" }, "devDependencies": { @@ -450,12 +450,12 @@ "integrity": "sha512-BYY7IavBjwsWWSmVcMz2A9mKiDD9RvacnsItgmy1xV8cmgbtxFfKmKMtkVpD7pYtkx4mIW4800yZBXueVFIWPw==" }, "node_modules/@json-schema-tools/dereferencer": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.6.1.tgz", - "integrity": "sha512-+h+K/H3pWoJVztTuz1ycTUc0ai/xH5eLZLurE4jQpqYwPcPvsXtFfbRxDhvxrrpjjg4PV3HmEjjORIEQPO4Dmw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.6.2.tgz", + "integrity": "sha512-cwvlTjPa92oQEMHsLBLNcTVAc4fmjFPXjPxOnYrnc1IDVSJsy1BbhfNfSUpVmIAOiXnygrQb488gbOxAj+tksg==", "dependencies": { "@json-schema-tools/reference-resolver": "^1.2.5", - "@json-schema-tools/traverse": "^1.10.0", + "@json-schema-tools/traverse": "^1.10.3", "fast-safe-stringify": "^2.1.1" } }, @@ -482,9 +482,9 @@ } }, "node_modules/@mapeo/core": { - "version": "9.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.6.tgz", - "integrity": "sha512-b1eHOmgVbtE02YAMR/kgfgHX5Mt//4gV290pgC+dPpZqccAgDIdBlaviJKRIGHCM16ZETdilx1NvSH+PJHYYGA==", + "version": "9.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.7.tgz", + "integrity": "sha512-8DXZPKtMMVLTgZX3F8Eew3KnLB6bmf9v7uda9TvMX14YM9np4G9IiYafPS/X7kFNvr7h823wMzkRotNnHZ3byg==", "hasInstallScript": true, "dependencies": { "@digidem/types": "^2.2.0", @@ -494,7 +494,7 @@ "@fastify/type-provider-typebox": "^3.3.0", "@hyperswarm/secret-stream": "^6.1.2", "@mapeo/crypto": "1.0.0-alpha.10", - "@mapeo/schema": "3.0.0-next.14", + "@mapeo/schema": "3.0.0-next.15", "@mapeo/sqlite-indexer": "1.0.0-alpha.8", "@sinclair/typebox": "^0.29.6", "b4a": "^1.6.3", @@ -504,7 +504,7 @@ "compact-encoding": "^2.12.0", "corestore": "^6.8.4", "debug": "^4.3.4", - "drizzle-orm": "0.28.2", + "drizzle-orm": "^0.30.8", "fastify": ">= 4", "fastify-plugin": "^4.5.0", "hyperblobs": "2.3.0", @@ -529,7 +529,7 @@ "throttle-debounce": "^5.0.0", "tiny-typed-emitter": "^2.1.0", "type-fest": "^4.5.0", - "undici": "^6.7.0", + "undici": "^6.13.0", "varint": "^6.0.0", "yauzl-promise": "^4.0.0" } @@ -628,9 +628,9 @@ } }, "node_modules/@mapeo/ipc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.2.0.tgz", - "integrity": "sha512-qAkKlwpIJw0Srr9pR4w9pbluIADzkMHpRbOuxSYY5+fonmNuEc8esRXWwvaL4SeBEIoUsuIOpWuEDOIoYlRk6Q==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.3.0.tgz", + "integrity": "sha512-8OdARTEBgCFCXrcJqwPuz397i10MiCIGG1Y9SSZ7myzN2o72lbuVAu7SX/D2gbHrVVD2tuia9EwaDBVsznas2w==", "dependencies": { "eventemitter3": "^5.0.1", "p-defer": "^4.0.0", @@ -640,13 +640,13 @@ "node": ">=18.17.1" }, "peerDependencies": { - "@mapeo/core": "9.0.0-alpha.6" + "@mapeo/core": "9.0.0-alpha.7" } }, "node_modules/@mapeo/schema": { - "version": "3.0.0-next.14", - "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.14.tgz", - "integrity": "sha512-i0AUHbwMxUyggk6SDURxLPXOVCWlLAszSKUYm2fviQXYrGNcUALniw8JBn3x5jzfCFW1xrTUNhIcnt4IuF95mA==", + "version": "3.0.0-next.15", + "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.15.tgz", + "integrity": "sha512-7g5GLkDhZLeStPIkVkTqElidng9cKtzNDJSQww5+Z7PWpkniMLDJYLki9TchEJEmPoAgwxgULNBiC69cMnNo9Q==", "dependencies": { "@json-schema-tools/dereferencer": "^1.6.1", "ajv": "^8.12.0", @@ -660,15 +660,15 @@ } }, "node_modules/@mapeo/schema/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -681,9 +681,9 @@ } }, "node_modules/@mapeo/schema/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -695,9 +695,9 @@ } }, "node_modules/@mapeo/schema/node_modules/type-fest": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.14.0.tgz", - "integrity": "sha512-on5/Cw89wwqGZQu+yWO0gGMGu8VNxsaW9SB2HE8yJjllEk7IDTwnSN1dUVldYILhYPN5HzD7WAaw2cc/jBfn0Q==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz", + "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", "engines": { "node": ">=16" }, @@ -2001,27 +2001,33 @@ } }, "node_modules/drizzle-orm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.28.2.tgz", - "integrity": "sha512-QRyuzvpJr7GE6LpvZ/sg2nAKNg2if1uGGkgFTiXn4auuYId//vVJe6HBsDTktfKfcaDGzIYos+/f+PS5EkBmrg==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.30.8.tgz", + "integrity": "sha512-9pBJA0IjnpPpzZ6s9jlS1CQAbKoBmbn2GJesPhXaVblAA/joOJ4AWWevYcqvLGj9SvThBAl7WscN8Zwgg5mnTw==", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", "@libsql/client": "*", "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@types/better-sqlite3": "*", "@types/pg": "*", + "@types/react": ">=18", "@types/sql.js": "*", - "@vercel/postgres": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", + "expo-sqlite": ">=13.2.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", + "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, @@ -2032,12 +2038,18 @@ "@cloudflare/workers-types": { "optional": true }, + "@electric-sql/pglite": { + "optional": true + }, "@libsql/client": { "optional": true }, "@neondatabase/serverless": { "optional": true }, + "@op-engineering/op-sqlite": { + "optional": true + }, "@opentelemetry/api": { "optional": true }, @@ -2050,18 +2062,27 @@ "@types/pg": { "optional": true }, + "@types/react": { + "optional": true + }, "@types/sql.js": { "optional": true }, "@vercel/postgres": { "optional": true }, + "@xata.io/client": { + "optional": true + }, "better-sqlite3": { "optional": true }, "bun-types": { "optional": true }, + "expo-sqlite": { + "optional": true + }, "knex": { "optional": true }, @@ -2077,6 +2098,9 @@ "postgres": { "optional": true }, + "react": { + "optional": true + }, "sql.js": { "optional": true }, @@ -4003,11 +4027,11 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -5296,9 +5320,9 @@ } }, "node_modules/undici": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.10.2.tgz", - "integrity": "sha512-HcVuBy7ACaDejIMdwCzAvO22OsiE6ir6ziTIr9kAE0vB+PheVe29ZvRN8p7FXCO2uZHTjEoUs5bPiFpuc/hwwQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.13.0.tgz", + "integrity": "sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==", "engines": { "node": ">=18.0" } @@ -5896,12 +5920,12 @@ "integrity": "sha512-BYY7IavBjwsWWSmVcMz2A9mKiDD9RvacnsItgmy1xV8cmgbtxFfKmKMtkVpD7pYtkx4mIW4800yZBXueVFIWPw==" }, "@json-schema-tools/dereferencer": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.6.1.tgz", - "integrity": "sha512-+h+K/H3pWoJVztTuz1ycTUc0ai/xH5eLZLurE4jQpqYwPcPvsXtFfbRxDhvxrrpjjg4PV3HmEjjORIEQPO4Dmw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.6.2.tgz", + "integrity": "sha512-cwvlTjPa92oQEMHsLBLNcTVAc4fmjFPXjPxOnYrnc1IDVSJsy1BbhfNfSUpVmIAOiXnygrQb488gbOxAj+tksg==", "requires": { "@json-schema-tools/reference-resolver": "^1.2.5", - "@json-schema-tools/traverse": "^1.10.0", + "@json-schema-tools/traverse": "^1.10.3", "fast-safe-stringify": "^2.1.1" } }, @@ -5925,9 +5949,9 @@ "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==" }, "@mapeo/core": { - "version": "9.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.6.tgz", - "integrity": "sha512-b1eHOmgVbtE02YAMR/kgfgHX5Mt//4gV290pgC+dPpZqccAgDIdBlaviJKRIGHCM16ZETdilx1NvSH+PJHYYGA==", + "version": "9.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.7.tgz", + "integrity": "sha512-8DXZPKtMMVLTgZX3F8Eew3KnLB6bmf9v7uda9TvMX14YM9np4G9IiYafPS/X7kFNvr7h823wMzkRotNnHZ3byg==", "requires": { "@digidem/types": "^2.2.0", "@electron/asar": "^3.2.8", @@ -5936,7 +5960,7 @@ "@fastify/type-provider-typebox": "^3.3.0", "@hyperswarm/secret-stream": "^6.1.2", "@mapeo/crypto": "1.0.0-alpha.10", - "@mapeo/schema": "3.0.0-next.14", + "@mapeo/schema": "3.0.0-next.15", "@mapeo/sqlite-indexer": "1.0.0-alpha.8", "@sinclair/typebox": "^0.29.6", "b4a": "^1.6.3", @@ -5946,7 +5970,7 @@ "compact-encoding": "^2.12.0", "corestore": "^6.8.4", "debug": "^4.3.4", - "drizzle-orm": "0.28.2", + "drizzle-orm": "^0.30.8", "fastify": ">= 4", "fastify-plugin": "^4.5.0", "hyperblobs": "2.3.0", @@ -5971,7 +5995,7 @@ "throttle-debounce": "^5.0.0", "tiny-typed-emitter": "^2.1.0", "type-fest": "^4.5.0", - "undici": "^6.7.0", + "undici": "^6.13.0", "varint": "^6.0.0", "yauzl-promise": "^4.0.0" }, @@ -6060,9 +6084,9 @@ } }, "@mapeo/ipc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.2.0.tgz", - "integrity": "sha512-qAkKlwpIJw0Srr9pR4w9pbluIADzkMHpRbOuxSYY5+fonmNuEc8esRXWwvaL4SeBEIoUsuIOpWuEDOIoYlRk6Q==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.3.0.tgz", + "integrity": "sha512-8OdARTEBgCFCXrcJqwPuz397i10MiCIGG1Y9SSZ7myzN2o72lbuVAu7SX/D2gbHrVVD2tuia9EwaDBVsznas2w==", "requires": { "eventemitter3": "^5.0.1", "p-defer": "^4.0.0", @@ -6070,9 +6094,9 @@ } }, "@mapeo/schema": { - "version": "3.0.0-next.14", - "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.14.tgz", - "integrity": "sha512-i0AUHbwMxUyggk6SDURxLPXOVCWlLAszSKUYm2fviQXYrGNcUALniw8JBn3x5jzfCFW1xrTUNhIcnt4IuF95mA==", + "version": "3.0.0-next.15", + "resolved": "https://registry.npmjs.org/@mapeo/schema/-/schema-3.0.0-next.15.tgz", + "integrity": "sha512-7g5GLkDhZLeStPIkVkTqElidng9cKtzNDJSQww5+Z7PWpkniMLDJYLki9TchEJEmPoAgwxgULNBiC69cMnNo9Q==", "requires": { "@json-schema-tools/dereferencer": "^1.6.1", "ajv": "^8.12.0", @@ -6086,29 +6110,29 @@ }, "dependencies": { "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "requires": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "requires": { "brace-expansion": "^2.0.1" } }, "type-fest": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.14.0.tgz", - "integrity": "sha512-on5/Cw89wwqGZQu+yWO0gGMGu8VNxsaW9SB2HE8yJjllEk7IDTwnSN1dUVldYILhYPN5HzD7WAaw2cc/jBfn0Q==" + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz", + "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==" }, "typedoc": { "version": "0.24.8", @@ -7021,9 +7045,9 @@ } }, "drizzle-orm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.28.2.tgz", - "integrity": "sha512-QRyuzvpJr7GE6LpvZ/sg2nAKNg2if1uGGkgFTiXn4auuYId//vVJe6HBsDTktfKfcaDGzIYos+/f+PS5EkBmrg==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.30.8.tgz", + "integrity": "sha512-9pBJA0IjnpPpzZ6s9jlS1CQAbKoBmbn2GJesPhXaVblAA/joOJ4AWWevYcqvLGj9SvThBAl7WscN8Zwgg5mnTw==", "requires": {} }, "duplexify": { @@ -8505,11 +8529,11 @@ "dev": true }, "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, @@ -9480,9 +9504,9 @@ "optional": true }, "undici": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.10.2.tgz", - "integrity": "sha512-HcVuBy7ACaDejIMdwCzAvO22OsiE6ir6ziTIr9kAE0vB+PheVe29ZvRN8p7FXCO2uZHTjEoUs5bPiFpuc/hwwQ==" + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.13.0.tgz", + "integrity": "sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==" }, "undici-types": { "version": "5.25.3", diff --git a/src/backend/package.json b/src/backend/package.json index af8204ed2..1d0613228 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -7,14 +7,14 @@ "main": "index.js", "scripts": { "build": "node ./scripts/bundle-backend.mjs --entry=index.js --output=index.bundle.js", - "lint": "tsc", + "lint:types": "tsc --noEmit", "postinstall": "patch-package" }, "author": "Digital Democracy", "license": "MIT", "dependencies": { - "@mapeo/core": "^9.0.0-alpha.6", - "@mapeo/ipc": "^0.2.0", + "@mapeo/core": "9.0.0-alpha.7", + "@mapeo/ipc": "0.3.0", "debug": "^4.3.4" }, "devDependencies": { diff --git a/src/backend/patches/@mapeo+core+9.0.0-alpha.6+001+fix-quickbit-dynamic-require.patch b/src/backend/patches/@mapeo+core+9.0.0-alpha.7+001+fix-quickbit-dynamic-require.patch similarity index 100% rename from src/backend/patches/@mapeo+core+9.0.0-alpha.6+001+fix-quickbit-dynamic-require.patch rename to src/backend/patches/@mapeo+core+9.0.0-alpha.7+001+fix-quickbit-dynamic-require.patch diff --git a/src/backend/patches/@rollup+plugin-esm-shim+0.1.4+001+fix-shim-insertion.patch b/src/backend/patches/@rollup+plugin-esm-shim+0.1.4+001+fix-shim-insertion.patch new file mode 100644 index 000000000..347b9640b --- /dev/null +++ b/src/backend/patches/@rollup+plugin-esm-shim+0.1.4+001+fix-shim-insertion.patch @@ -0,0 +1,49 @@ +diff --git a/node_modules/@rollup/plugin-esm-shim/dist/es/index.js b/node_modules/@rollup/plugin-esm-shim/dist/es/index.js +index 162763c..6e70fd8 100644 +--- a/node_modules/@rollup/plugin-esm-shim/dist/es/index.js ++++ b/node_modules/@rollup/plugin-esm-shim/dist/es/index.js +@@ -28,22 +28,34 @@ function matchAllPolyfill(input, pattern) { + } + return output; + } +-function findPositionToInsertShim(input, pattern) { +- let lastImport; +- // mimicking behavior of `String.matchAll` as it returns an iterator, not an array +- for (const match of matchAllPolyfill(input, pattern)) { +- lastImport = match; ++function findPositionToInsertShim(input) { ++ // Workaround for . ++ // Finds the first index after the `import`s at the top of the file. ++ let result = 0; ++ ++ let hasSeenImport = false; ++ ++ // Doesn't handle CRLF newlines, but that shouldn't affect the result. ++ const lines = input.split(/\n/g); ++ ++ for (const line of lines) { ++ const trimmed = line.trim(); ++ ++ const isImportOrBlankOrComment = !trimmed || trimmed.startsWith('import ') || trimmed.startsWith('//'); ++ ++ if (hasSeenImport && !isImportOrBlankOrComment) break; ++ ++ result += line.length + 1; // add 1 for newline ++ hasSeenImport = hasSeenImport || isImportOrBlankOrComment; + } +- if (!lastImport) { +- return 0; +- } +- return (lastImport.index || 0) + lastImport[0].length; ++ ++ return result; + } + function provideCJSSyntax(code) { + if (code.includes(ESMShim) || !CJSyntaxRegex.test(code)) { + return null; + } +- const indexToAppend = findPositionToInsertShim(code, ESMStaticImportRegex); ++ const indexToAppend = findPositionToInsertShim(code); + const s = new MagicString(code); + s.appendRight(indexToAppend, ESMShim); + const sourceMap = s.generateMap({ diff --git a/src/backend/patches/README.md b/src/backend/patches/README.md index 0f92ce6b0..a55af6e84 100644 --- a/src/backend/patches/README.md +++ b/src/backend/patches/README.md @@ -4,7 +4,7 @@ These patches use [patch-package](https://github.com/ds300/patch-package) to upd ## `@mapeo/core` -### [Fix quickbit dynamic require](./@mapeo+core+9.0.0-alpha.6+001+fix-quickbit-dynamic-require.patch) +### [Fix quickbit dynamic require](./@mapeo+core+9.0.0-alpha.7+001+fix-quickbit-dynamic-require.patch) - Rollup complains about the dynamic require of `quickbit-universal` in this file. Easier to just simplify the import @@ -13,3 +13,9 @@ These patches use [patch-package](https://github.com/ds300/patch-package) to upd ### [Remove conditional `original-fs` import](./@electron+asar+3.2.9+001+remove-original-fs-require.patch) `original-fs` is conditionally imported (based on Electron-specific checks) but Rollup is not smart enough to lazily require the module in the bundled output. This causes errors at runtime because an import of a non-existent module occurs. + +## `@rollup/plugin-esm` + +### [Fix shim insertion](./@rollup+plugin-esm-shim+0.1.4+001+fix-shim-insertion.patch) + +This is a workaround for [a bug in the plugin](https://github.com/rollup/plugins/issues/1709). diff --git a/src/backend/src/node-rs-crc32-shim.js b/src/backend/src/node-rs-crc32-shim.js index 208c95d9b..f7bc402d6 100644 --- a/src/backend/src/node-rs-crc32-shim.js +++ b/src/backend/src/node-rs-crc32-shim.js @@ -16,7 +16,7 @@ let crcTable * @param {number | undefined | null} [initialState] * @returns {number} */ -function crc32(input, initialState = 0) { +function crc32(input, initialState) { if (typeof input === 'string') return crc32(Buffer.from(input), initialState) if (!crcTable) { @@ -30,7 +30,7 @@ function crc32(input, initialState = 0) { } } - let crc = ~~initialState ^ -1 + let crc = ~~(initialState || 0) ^ -1 for (let i = 0; i < input.length; i++) { crc = crcTable[(crc ^ input[i]) & 0xff] ^ (crc >>> 8) } diff --git a/src/backend/tsconfig.json b/src/backend/tsconfig.json index a84cf6d25..4b44d9b33 100644 --- a/src/backend/tsconfig.json +++ b/src/backend/tsconfig.json @@ -7,7 +7,6 @@ "./node_modules/@digidem/types/vendor/*/index.d.ts" ] }, - "resolveJsonModule": true, "allowJs": true, "checkJs": true, "noEmit": true, diff --git a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx index 6c4de66de..93fd8c817 100644 --- a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx +++ b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx @@ -1,9 +1,9 @@ -import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; +import { + BottomTabNavigationProp, + createBottomTabNavigator, +} from '@react-navigation/bottom-tabs'; import {NavigatorScreenParams} from '@react-navigation/native'; import * as React from 'react'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import {useForegroundPermissions} from 'expo-location'; - import {HomeHeader} from '../../sharedComponents/HomeHeader'; import {RootStack} from '../AppStack'; import {MessageDescriptor} from 'react-intl'; @@ -33,7 +33,7 @@ import {ProjectCreated} from '../../screens/Settings/CreateOrJoinProject/CreateP import {JoinExistingProject} from '../../screens/Settings/CreateOrJoinProject/JoinExistingProject'; import {YourTeam} from '../../screens/Settings/ProjectSettings/YourTeam'; import {SelectDevice} from '../../screens/Settings/ProjectSettings/YourTeam/SelectDevice'; -import {DeviceRole, DeviceType} from '../../sharedTypes'; +import {DeviceType, DeviceRoleForNewInvite} from '../../sharedTypes'; import {SelectInviteeRole} from '../../screens/Settings/ProjectSettings/YourTeam/SelectInviteeRole'; import {ReviewInvitation} from '../../screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/ReviewInvitation'; import {InviteAccepted} from '../../screens/Settings/ProjectSettings/YourTeam/InviteAccepted'; @@ -50,19 +50,36 @@ import { GpsModal, createNavigationOptions as createGpsModalNavigationOptions, } from '../../screens/GpsModal'; -import {useLocation} from '../../hooks/useLocation'; -import {useLocationProviderStatus} from '../../hooks/useLocationProviderStatus'; -import {getLocationStatus} from '../../lib/utils'; +import {useCurrentTab} from '../../hooks/useCurrentTab'; +import {TrackingTabBarIcon} from './TabBar/TrackingTabBarIcon'; +import {TabName} from '../types'; +import {CameraTabBarIcon} from './TabBar/CameraTabBarIcon'; +import {MapTabBarIcon} from './TabBar/MapTabBarIcon'; +import {InviteDeclined} from '../../screens/Settings/ProjectSettings/YourTeam/InviteDeclined'; +import {UnableToCancelInvite} from '../../screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite'; +import { + SyncScreen, + createNavigationOptions as createSyncNavOptions, +} from '../../screens/Sync'; + +export const TAB_BAR_HEIGHT = 70; export type HomeTabsList = { Map: undefined; Camera: undefined; + Tracking: undefined; +}; + +type InviteProps = { + name: string; + deviceType: DeviceType; + deviceId: string; + role: DeviceRoleForNewInvite; }; export type AppList = { Home: NavigatorScreenParams; GpsModal: undefined; - SyncModal: undefined; Settings: undefined; ProjectConfig: undefined; AboutMapeo: undefined; @@ -116,61 +133,68 @@ export type AppList = { YourTeam: undefined; SelectDevice: undefined; SelectInviteeRole: {name: string; deviceType: DeviceType; deviceId: string}; - ReviewAndInvite: { - name: string; - deviceType: DeviceType; - deviceId: string; - role: DeviceRole; - }; - InviteAccepted: { - name: string; - deviceType: DeviceType; - deviceId: string; - role: DeviceRole; - }; + ReviewAndInvite: InviteProps; + InviteAccepted: InviteProps; + InviteDeclined: InviteProps; + UnableToCancelInvite: InviteProps; DeviceNameDisplay: undefined; DeviceNameEdit: undefined; + Sync: undefined; }; const Tab = createBottomTabNavigator(); const HomeTabs = () => { - const locationState = useLocation({maxDistanceInterval: 0}); - const [permissions] = useForegroundPermissions(); - const locationProviderStatus = useLocationProviderStatus(); - - const precision = locationState.location?.coords.accuracy; - - const locationStatus = - !!locationState.error || !permissions?.granted - ? 'error' - : getLocationStatus({ - location: locationState.location, - providerStatus: locationProviderStatus, - }); + const {handleTabPress} = useCurrentTab(); return ( ({ - tabBarIcon: ({color}) => { - const iconName = route.name === 'Map' ? 'map' : 'photo-camera'; - return ; - }, - header: () => ( - - ), + tabBarStyle: {height: TAB_BAR_HEIGHT}, + tabBarShowLabel: false, headerTransparent: true, tabBarTestID: 'tabBarButton' + route.name, + header: HomeHeader, })} - initialRouteName="Map" + initialRouteName={TabName.Map} backBehavior="initialRoute"> - - + + + {process.env.FEATURE_TRACKS && ( + ; + }) => ({ + tabPress: e => { + e.preventDefault(); + navigation.navigate(TabName.Map); + }, + })} + children={() => <>} + /> + )} ); }; @@ -197,9 +221,9 @@ export const createDefaultScreenGroup = ( options={props => { const observationId = props.route.params?.observationId; return { - headerLeft: props => ( + headerLeft: headerProp => ( ), @@ -336,5 +360,20 @@ export const createDefaultScreenGroup = ( component={GpsModal} options={createGpsModalNavigationOptions({intl})} /> + + + ); diff --git a/src/frontend/Navigation/ScreenGroups/TabBar/CameraTabBarIcon.tsx b/src/frontend/Navigation/ScreenGroups/TabBar/CameraTabBarIcon.tsx new file mode 100644 index 000000000..25694426c --- /dev/null +++ b/src/frontend/Navigation/ScreenGroups/TabBar/CameraTabBarIcon.tsx @@ -0,0 +1,16 @@ +import React, {FC} from 'react'; +import {TabBarIconProps, TabName} from '../../types'; +import {TabBarIcon} from './TabBarIcon'; +import {useTabNavigationStore} from '../../../hooks/useTabNavigationStore.ts'; + +export const CameraTabBarIcon: FC = props => { + const {currentTab} = useTabNavigationStore(); + + return ( + + ); +}; diff --git a/src/frontend/Navigation/ScreenGroups/TabBar/MapTabBarIcon.tsx b/src/frontend/Navigation/ScreenGroups/TabBar/MapTabBarIcon.tsx new file mode 100644 index 000000000..a7c7dbd82 --- /dev/null +++ b/src/frontend/Navigation/ScreenGroups/TabBar/MapTabBarIcon.tsx @@ -0,0 +1,16 @@ +import React, {FC} from 'react'; +import {TabBarIconProps, TabName} from '../../types'; +import {TabBarIcon} from './TabBarIcon'; +import {useTabNavigationStore} from '../../../hooks/useTabNavigationStore.ts'; + +export const MapTabBarIcon: FC = props => { + const {currentTab} = useTabNavigationStore(); + + return ( + + ); +}; diff --git a/src/frontend/Navigation/ScreenGroups/TabBar/TabBarIcon.tsx b/src/frontend/Navigation/ScreenGroups/TabBar/TabBarIcon.tsx new file mode 100644 index 000000000..e2d6604ea --- /dev/null +++ b/src/frontend/Navigation/ScreenGroups/TabBar/TabBarIcon.tsx @@ -0,0 +1,18 @@ +import React, {FC} from 'react'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import {TabBarIconProps} from '../../types'; +import {COMAPEO_BLUE, MEDIUM_GREY} from '../../../lib/styles'; + +export interface TabBarIcon extends TabBarIconProps { + iconName: string; +} + +export const TabBarIcon: FC = ({size, iconName, focused}) => { + return ( + + ); +}; diff --git a/src/frontend/Navigation/ScreenGroups/TabBar/TrackingTabBarIcon.tsx b/src/frontend/Navigation/ScreenGroups/TabBar/TrackingTabBarIcon.tsx new file mode 100644 index 000000000..d18095e80 --- /dev/null +++ b/src/frontend/Navigation/ScreenGroups/TabBar/TrackingTabBarIcon.tsx @@ -0,0 +1,49 @@ +import React, {FC} from 'react'; +import {StyleSheet, View} from 'react-native'; +import {TabBarIcon} from './TabBarIcon'; +import {useTracking} from '../../../hooks/tracks/useTracking'; +import {Text} from '../../../sharedComponents/Text'; +import {TabBarIconProps, TabName} from '../../types'; +import {useTrackTimerContext} from '../../../contexts/TrackTimerContext'; +import {useTabNavigationStore} from '../../../hooks/useTabNavigationStore.ts'; + +export const TrackingTabBarIcon: FC = props => { + const {isTracking} = useTracking(); + const {timer} = useTrackTimerContext(); + const {currentTab} = useTabNavigationStore(); + + return ( + <> + {isTracking && ( + + + {timer} + + )} + + + ); +}; + +const styles = StyleSheet.create({ + runtimeWrapper: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + indicator: { + marginRight: 5, + height: 10, + width: 10, + borderRadius: 99, + backgroundColor: '#59A553', + }, + timer: { + marginLeft: 5, + fontSize: 12, + }, +}); diff --git a/src/frontend/Navigation/types.ts b/src/frontend/Navigation/types.ts new file mode 100644 index 000000000..581d18cb6 --- /dev/null +++ b/src/frontend/Navigation/types.ts @@ -0,0 +1,11 @@ +export interface TabBarIconProps { + size: number; + focused: boolean; + color: string; +} + +export enum TabName { + Map = 'Map', + Camera = 'Camera', + Tracking = 'Tracking', +} diff --git a/src/frontend/contexts/ExternalProviders.tsx b/src/frontend/contexts/ExternalProviders.tsx index 61a09d03d..7611df536 100644 --- a/src/frontend/contexts/ExternalProviders.tsx +++ b/src/frontend/contexts/ExternalProviders.tsx @@ -11,6 +11,9 @@ import { // See https://github.com/gorhom/react-native-bottom-sheet/issues/1157 import {BottomSheetModalProvider} from '@gorhom/bottom-sheet'; import {AppStackList} from '../Navigation/AppStack'; +import {GPSModalContextProvider} from './GPSModalContext'; +import {TrackTimerContextProvider} from './TrackTimerContext'; +import {SharedLocationContextProvider} from './SharedLocationContext'; type ExternalProvidersProp = { children: React.ReactNode; @@ -26,9 +29,17 @@ export const ExternalProviders = ({ return ( - - {children} - + + + + + + {children} + + + + + ); diff --git a/src/frontend/contexts/GPSModalContext.tsx b/src/frontend/contexts/GPSModalContext.tsx new file mode 100644 index 000000000..cdbdaa338 --- /dev/null +++ b/src/frontend/contexts/GPSModalContext.tsx @@ -0,0 +1,31 @@ +import {BottomSheetModal} from '@gorhom/bottom-sheet'; +import {BottomSheetModalMethods} from '@gorhom/bottom-sheet/lib/typescript/types'; +import React, {createContext, useContext, useRef} from 'react'; + +interface GPSModalContext { + bottomSheetRef: React.RefObject; +} + +const GPSModalContext = createContext(null); + +const GPSModalContextProvider = ({children}: {children: React.ReactNode}) => { + const bottomSheetRef = useRef(null); + + return ( + + {children} + + ); +}; + +function useGPSModalContext() { + const context = useContext(GPSModalContext); + if (!context) { + throw new Error( + 'useGPSModalContext must be used within a GPSModalContextProvider', + ); + } + return context; +} + +export {GPSModalContextProvider, useGPSModalContext}; diff --git a/src/frontend/contexts/ProjectContext.tsx b/src/frontend/contexts/ProjectContext.tsx index 4550c6090..09a2d3e63 100644 --- a/src/frontend/contexts/ProjectContext.tsx +++ b/src/frontend/contexts/ProjectContext.tsx @@ -67,7 +67,7 @@ export const ActiveProjectProvider = ({ return () => { cancelled = true; }; - }, [activeProjectId, setActiveProjectId]); + }, [activeProjectId, setActiveProjectId, mapeoApi]); if (!activeProject) { return ; diff --git a/src/frontend/contexts/SharedLocationContext.tsx b/src/frontend/contexts/SharedLocationContext.tsx new file mode 100644 index 000000000..bf9bfceb2 --- /dev/null +++ b/src/frontend/contexts/SharedLocationContext.tsx @@ -0,0 +1,74 @@ +import {createContext, useContext, useEffect, useRef, useState} from 'react'; +import {LocationState, useLocation} from '../hooks/useLocation'; +import React from 'react'; +import { + getBackgroundPermissionsAsync, + getForegroundPermissionsAsync, +} from 'expo-location'; +import {AppState} from 'react-native'; + +interface SharedLocationContext { + locationState: LocationState; + bgPermissions: boolean | null; + fgPermissions: boolean | null; +} + +const SharedLocationContext = createContext(null); + +const SharedLocationContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const location = useLocation({maxDistanceInterval: 3}); + const appState = useRef(AppState.currentState); + const [bgPermissions, setBgPermissions] = useState(null); + const [fgPermissions, setFgPermissions] = useState(null); + + const refreshPermissionState = () => { + getBackgroundPermissionsAsync().then(({granted}) => + setBgPermissions(granted), + ); + getForegroundPermissionsAsync().then(({granted}) => + setFgPermissions(granted), + ); + }; + + useEffect(refreshPermissionState, []); + useEffect(() => { + const subscription = AppState.addEventListener('change', newState => { + if ( + appState.current.match(/inactive|background/) && + newState === 'active' + ) { + refreshPermissionState(); + } + appState.current = newState; + }); + + return () => subscription.remove(); + }, []); + + return ( + + {children} + + ); +}; + +function useSharedLocationContext() { + const context = useContext(SharedLocationContext); + if (!context) { + throw new Error( + 'useSharedLocationContext must be used within a SharedLocationContextProvider', + ); + } + return context; +} + +export {SharedLocationContextProvider, useSharedLocationContext}; diff --git a/src/frontend/contexts/TrackTimerContext.tsx b/src/frontend/contexts/TrackTimerContext.tsx new file mode 100644 index 000000000..a95048160 --- /dev/null +++ b/src/frontend/contexts/TrackTimerContext.tsx @@ -0,0 +1,32 @@ +import React, {createContext, useContext} from 'react'; +import {useCurrentTrackStore} from '../hooks/tracks/useCurrentTrackStore'; +import {useFormattedTimeSince} from '../hooks/useFormattedTimeSince'; + +interface TrackTimerContext { + timer: string; +} + +const TrackTimerContext = createContext(null); + +const TrackTimerContextProvider = ({children}: {children: React.ReactNode}) => { + const trackingSince = useCurrentTrackStore(state => state.trackingSince); + const timer = useFormattedTimeSince(trackingSince, 1000); + + return ( + + {children} + + ); +}; + +function useTrackTimerContext() { + const context = useContext(TrackTimerContext); + if (!context) { + throw new Error( + 'useTrackTimerContext must be used within a TrackTimerContextProvider', + ); + } + return context; +} + +export {TrackTimerContextProvider, useTrackTimerContext}; diff --git a/src/frontend/hooks/server/invites.ts b/src/frontend/hooks/server/invites.ts index 6394a1265..9f73f7d8f 100644 --- a/src/frontend/hooks/server/invites.ts +++ b/src/frontend/hooks/server/invites.ts @@ -4,6 +4,7 @@ import { useSuspenseQuery, } from '@tanstack/react-query'; import {useApi} from '../../contexts/ApiContext'; +import {PROJECTS_KEY, useProject, useUpdateActiveProjectId} from './projects'; export const INVITE_KEY = 'pending_invites'; @@ -17,16 +18,27 @@ export function usePendingInvites() { }); } -export function useAcceptInvite() { +export function useAcceptInvite(projectId?: string) { const mapeoApi = useApi(); const queryClient = useQueryClient(); + const switchActiveProject = useUpdateActiveProjectId(); + return useMutation({ mutationFn: async ({inviteId}: {inviteId: string}) => { if (!inviteId) return; mapeoApi.invite.accept({inviteId}); }, onSuccess: () => { - queryClient.invalidateQueries({queryKey: [INVITE_KEY]}); + // This is a workaround. There is a race condition where the project in not available when the invite is accepted. This is temporary and is currently being worked on. + setTimeout(() => { + queryClient + .invalidateQueries({queryKey: [INVITE_KEY, PROJECTS_KEY]}) + .then(() => { + if (projectId) { + switchActiveProject(projectId); + } + }); + }, 5000); }, }); } @@ -61,3 +73,33 @@ export function useClearAllPendingInvites() { }, }); } + +export function useSendInvite() { + const queryClient = useQueryClient(); + const project = useProject(); + type InviteParams = Parameters; + return useMutation({ + mutationFn: ({ + deviceId, + role, + }: { + deviceId: InviteParams[0]; + role: InviteParams[1]; + }) => project.$member.invite(deviceId, role), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [INVITE_KEY]}); + }, + }); +} + +export function useRequestCancelInvite() { + const queryClient = useQueryClient(); + const project = useProject(); + return useMutation({ + mutationFn: (deviceId: string) => + project.$member.requestCancelInvite(deviceId), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [INVITE_KEY]}); + }, + }); +} diff --git a/src/frontend/hooks/server/observations.ts b/src/frontend/hooks/server/observations.ts index 11870dcaf..a758d0981 100644 --- a/src/frontend/hooks/server/observations.ts +++ b/src/frontend/hooks/server/observations.ts @@ -8,6 +8,8 @@ import { import {useProject} from './projects'; import {ClientGeneratedObservation} from '../../sharedTypes'; +export const OBSERVATION_KEY = 'observations'; + export function useObservations() { const project = useProject(); @@ -24,7 +26,7 @@ export function useObservation(observationId: string) { const project = useProject(); return useSuspenseQuery({ - queryKey: ['observations', observationId], + queryKey: [OBSERVATION_KEY, observationId], queryFn: async () => { if (!project) throw new Error('Project instance does not exist'); return project.observation.getByDocId(observationId); @@ -46,7 +48,7 @@ export function useCreateObservation() { }); }, onSuccess: () => { - queryClient.invalidateQueries({queryKey: ['observations']}); + queryClient.invalidateQueries({queryKey: [OBSERVATION_KEY]}); }, }); } @@ -67,7 +69,7 @@ export function useEditObservation() { return project.observation.update(id, value); }, onSuccess: () => { - queryClient.invalidateQueries({queryKey: ['observations']}); + queryClient.invalidateQueries({queryKey: [OBSERVATION_KEY]}); }, }); } @@ -82,7 +84,7 @@ export function useDeleteObservation() { return project.observation.delete(id); }, onSuccess: () => { - queryClient.invalidateQueries({queryKey: ['observations']}); + queryClient.invalidateQueries({queryKey: [OBSERVATION_KEY]}); }, }); } diff --git a/src/frontend/hooks/server/projects.ts b/src/frontend/hooks/server/projects.ts index e54f721f6..3a7204dc6 100644 --- a/src/frontend/hooks/server/projects.ts +++ b/src/frontend/hooks/server/projects.ts @@ -2,6 +2,8 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {useApi} from '../../contexts/ApiContext'; import {useActiveProjectContext} from '../../contexts/ProjectContext'; +export const PROJECTS_KEY = 'all_projects'; + export function useUpdateActiveProjectId() { const projectContext = useActiveProjectContext(); return projectContext.switchProject; @@ -17,7 +19,7 @@ export function useAllProjects() { return useQuery({ queryFn: async () => await api.listProjects(), - queryKey: ['projects'], + queryKey: [PROJECTS_KEY], }); } @@ -50,10 +52,11 @@ export function useProjectMembers() { export function useProjectSettings() { const project = useProject(); + return useQuery({ - queryFn: async () => { - return await project.$getProjectSettings(); - }, queryKey: ['projectSettings'], + queryFn: () => { + return project.$getProjectSettings(); + }, }); } diff --git a/src/frontend/hooks/tracks/useCurrentTrackStore.ts b/src/frontend/hooks/tracks/useCurrentTrackStore.ts new file mode 100644 index 000000000..2febd86ab --- /dev/null +++ b/src/frontend/hooks/tracks/useCurrentTrackStore.ts @@ -0,0 +1,64 @@ +import {create} from 'zustand'; +import {calculateTotalDistance} from '../../utils/distance'; +import {LocationHistoryPoint} from '../../sharedTypes/location'; + +type TracksStoreState = { + locationHistory: LocationHistoryPoint[]; + observations: string[]; + distance: number; + addNewObservation: (observationId: string) => void; + addNewLocations: (locationData: LocationHistoryPoint[]) => void; + clearLocationHistory: () => void; + setTracking: (val: boolean) => void; +} & ( + | { + isTracking: true; + trackingSince: Date; + } + | { + isTracking: false; + trackingSince: null; + } +); + +export const useCurrentTrackStore = create(set => ({ + isTracking: false, + locationHistory: [], + observations: [], + distance: 0, + trackingSince: null, + addNewObservation: (id: string) => + set(state => ({observations: [...state.observations, id]})), + addNewLocations: data => + set(({locationHistory, distance}) => { + if (data.length > 1) { + return { + locationHistory: [...locationHistory, ...data], + distance: distance + calculateTotalDistance(data), + }; + } + + if (locationHistory.length < 1) { + return { + locationHistory: [...locationHistory, ...data], + }; + } + + const lastLocation = locationHistory[locationHistory.length - 1]; + if (!lastLocation) { + throw Error('No lastLocation for state.locationHistory.length > 1'); + } + + return { + locationHistory: [...locationHistory, ...data], + distance: distance + calculateTotalDistance([lastLocation, ...data]), + }; + }), + clearLocationHistory: () => set(() => ({locationHistory: []})), + setTracking: (val: boolean) => + set(() => + val + ? {isTracking: true, trackingSince: new Date()} + : {isTracking: false, trackingSince: null}, + ), +})); diff --git a/src/frontend/hooks/tracks/useTracking.ts b/src/frontend/hooks/tracks/useTracking.ts new file mode 100644 index 000000000..51994c40f --- /dev/null +++ b/src/frontend/hooks/tracks/useTracking.ts @@ -0,0 +1,67 @@ +import * as Location from 'expo-location'; +import * as TaskManager from 'expo-task-manager'; +import {useCallback, useState} from 'react'; +import {useCurrentTrackStore} from './useCurrentTrackStore'; +import React from 'react'; +import {FullLocationData} from '../../sharedTypes/location'; + +export const LOCATION_TASK_NAME = 'background-location-task'; + +type LocationCallbackInfo = { + data: {locations: FullLocationData[]} | null; + error: TaskManager.TaskManagerError | null; +}; + +export function useTracking() { + const [loading, setLoading] = useState(false); + const addNewLocations = useCurrentTrackStore(state => state.addNewLocations); + const setTracking = useCurrentTrackStore(state => state.setTracking); + const isTracking = useCurrentTrackStore(state => state.isTracking); + + const addNewTrackLocations = useCallback( + ({data, error}: LocationCallbackInfo) => { + if (error) { + console.error('Error while processing location update callback', error); + } + if (data?.locations) { + addNewLocations( + data.locations.map(loc => ({ + latitude: loc.coords.latitude, + longitude: loc.coords.longitude, + timestamp: loc.timestamp, + })), + ); + } + }, + [addNewLocations], + ); + + React.useEffect(() => { + TaskManager.defineTask(LOCATION_TASK_NAME, addNewTrackLocations); + }, [addNewTrackLocations]); + + const startTracking = useCallback(async () => { + if (isTracking) { + console.warn('Start tracking attempt while tracking already enabled'); + setLoading(false); + return; + } + + setLoading(true); + + await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, { + accuracy: Location.Accuracy.Highest, + activityType: Location.LocationActivityType.Fitness, + }); + + setTracking(true); + setLoading(false); + }, [isTracking, setTracking]); + + const cancelTracking = useCallback(async () => { + await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); + setTracking(false); + }, [setTracking]); + + return {isTracking, startTracking, cancelTracking, loading}; +} diff --git a/src/frontend/hooks/useCurrentTab.ts b/src/frontend/hooks/useCurrentTab.ts new file mode 100644 index 000000000..33520bd85 --- /dev/null +++ b/src/frontend/hooks/useCurrentTab.ts @@ -0,0 +1,21 @@ +import {EventArg} from '@react-navigation/native'; +import {useGPSModalContext} from '../contexts/GPSModalContext'; +import {useTabNavigationStore} from './useTabNavigationStore.ts'; +import {TabName} from '../Navigation/types'; + +export const useCurrentTab = () => { + const {setCurrentTab} = useTabNavigationStore(); + const {bottomSheetRef} = useGPSModalContext(); + + const handleTabPress = ({target}: EventArg<'tabPress', true, undefined>) => { + const targetTab = target?.split('-')[0]; + if (targetTab === TabName.Tracking) { + bottomSheetRef.current?.present(); + } else { + bottomSheetRef.current?.close(); + } + setCurrentTab(targetTab as TabName); + }; + + return {handleTabPress}; +}; diff --git a/src/frontend/hooks/useFormattedTimeSince.ts b/src/frontend/hooks/useFormattedTimeSince.ts new file mode 100644 index 000000000..363baf2ff --- /dev/null +++ b/src/frontend/hooks/useFormattedTimeSince.ts @@ -0,0 +1,18 @@ +import {useEffect, useState} from 'react'; +import {Duration} from 'luxon'; + +export const useFormattedTimeSince = (start: Date | null, interval: number) => { + const [currentTime, setCurrentTime] = useState(new Date()); + let startDate = start ? start : new Date(); + + useEffect(() => { + setCurrentTime(new Date()); + const timer = setInterval(() => { + setCurrentTime(new Date()); + }, interval); + return () => clearInterval(timer); + }, [interval]); + + const millisPassed = Math.abs(currentTime.getTime() - startDate.getTime()); + return Duration.fromMillis(millisPassed).toFormat('hh:mm:ss'); +}; diff --git a/src/frontend/hooks/useLocation.ts b/src/frontend/hooks/useLocation.ts index bd6a2f5ac..aa1c5b546 100644 --- a/src/frontend/hooks/useLocation.ts +++ b/src/frontend/hooks/useLocation.ts @@ -1,4 +1,3 @@ -import {useFocusEffect} from '@react-navigation/native'; import CheapRuler from 'cheap-ruler'; import { watchPositionAsync, @@ -6,7 +5,7 @@ import { type LocationObject, Accuracy, } from 'expo-location'; -import React from 'react'; +import React, {useEffect} from 'react'; interface LocationOptions { /** Only update location if it has changed by at least this distance in meters (or maxTimeInterval has passed) */ @@ -37,46 +36,44 @@ export function useLocation({ const [permissions] = useForegroundPermissions(); - useFocusEffect( - React.useCallback(() => { - if (!permissions || !permissions.granted) return; + useEffect(() => { + if (!permissions || !permissions.granted) return; - let ignore = false; - const locationSubscriptionProm = watchPositionAsync( - { - accuracy: Accuracy.BestForNavigation, - distanceInterval, - }, - debounceLocation({ - minTimeInterval, - maxTimeInterval, - maxDistanceInterval, - })(location => { - if (ignore) return; - setLocation({location, error: undefined}); - }), - ); - - // Should not happen because we are checking permissions above, but just in case - locationSubscriptionProm.catch(error => { + let ignore = false; + const locationSubscriptionProm = watchPositionAsync( + { + accuracy: Accuracy.BestForNavigation, + distanceInterval, + }, + debounceLocation({ + minTimeInterval, + maxTimeInterval, + maxDistanceInterval, + })(location => { if (ignore) return; - setLocation(({location}) => { - return {location, error}; - }); + setLocation({location, error: undefined}); + }), + ); + + // Should not happen because we are checking permissions above, but just in case + locationSubscriptionProm.catch(error => { + if (ignore) return; + setLocation(({location}) => { + return {location, error}; }); + }); - return () => { - ignore = true; - locationSubscriptionProm.then(sub => sub.remove()); - }; - }, [ - permissions, - distanceInterval, - minTimeInterval, - maxTimeInterval, - maxDistanceInterval, - ]), - ); + return () => { + ignore = true; + locationSubscriptionProm.then(sub => sub.remove()); + }; + }, [ + distanceInterval, + maxDistanceInterval, + maxTimeInterval, + minTimeInterval, + permissions, + ]); return location; } diff --git a/src/frontend/hooks/useProjectInvite.ts b/src/frontend/hooks/useProjectInvite.ts index db0c74e20..67da14ec0 100644 --- a/src/frontend/hooks/useProjectInvite.ts +++ b/src/frontend/hooks/useProjectInvite.ts @@ -9,7 +9,7 @@ export function useProjectInvite() { const invites = usePendingInvites().data; // this will eventually sort invite by date const invite = invites[0]; - const acceptMutation = useAcceptInvite(); + const acceptMutation = useAcceptInvite(invite?.projectPublicId); const rejectMutation = useRejectInvite(); const clearAllInvites = useClearAllPendingInvites(); diff --git a/src/frontend/hooks/useSyncState.ts b/src/frontend/hooks/useSyncState.ts index 0ebcb540c..92e257c29 100644 --- a/src/frontend/hooks/useSyncState.ts +++ b/src/frontend/hooks/useSyncState.ts @@ -1,16 +1,25 @@ import {MapeoProjectApi} from '@mapeo/ipc'; -import React from 'react'; +import {useCallback, useSyncExternalStore} from 'react'; + import {useProject} from './server/projects'; -type SyncState = Awaited>; +export type SyncState = Awaited< + ReturnType +>; -const projectStateMap = new WeakMap< - MapeoProjectApi, - ReturnType ->(); +const projectSyncStoreMap = new WeakMap(); -function identity(state: SyncState | undefined) { - return state; +function useSyncStore() { + const project = useProject(); + + let syncStore = projectSyncStoreMap.get(project); + + if (!syncStore) { + syncStore = new SyncStore(project); + projectSyncStoreMap.set(project, syncStore); + } + + return syncStore; } /** @@ -29,68 +38,131 @@ function identity(state: SyncState | undefined) { * @param selector Select a subset of the state to subscribe to. Defaults to return the entire state. * @returns */ -export function useSyncState( - selector: (state: SyncState | undefined) => S = identity as any, +export function useSyncState( + selector: (state: SyncState | null) => S = identity as any, ): S { - const project = useProject(); + const syncStore = useSyncStore(); - let state = projectStateMap.get(project); - if (!state) { - state = createSyncState(project); - projectStateMap.set(project, state); - } - - const {subscribe, getSnapshot} = state; + const {subscribe, getStateSnapshot} = syncStore; - const getSelectorSnapshot = React.useCallback( - () => selector(getSnapshot()), - [selector, getSnapshot], + const getSelectorSnapshot = useCallback( + () => selector(getStateSnapshot()), + [selector, getStateSnapshot], ); - return React.useSyncExternalStore(subscribe, getSelectorSnapshot); + return useSyncExternalStore(subscribe, getSelectorSnapshot); } -function createSyncState(project: MapeoProjectApi) { - let state: SyncState | undefined; - let isSubscribedInternal = false; - const listeners = new Set<() => void>(); - let error: Error | undefined; +/** + * Calculates progress of *data* sync based on sync state. + * + * @returns A number between 0 and 1 when data sync is enabled. `null` otherwise. + */ +export function useSyncProgress() { + const {subscribe, getProgressSnapshot} = useSyncStore(); + return useSyncExternalStore(subscribe, getProgressSnapshot); +} + +class SyncStore { + #project: MapeoProjectApi; + + #listeners = new Set<() => void>(); + #isSubscribedInternal = false; + #error: Error | null = null; + #state: SyncState | null = null; + + /** + * Represents maximum value of `#state.data.want + #state.data.wanted` while data syncing is enabled. + * Resets to null when data syncing goes from enabled to disabled. + */ + #maxDataSyncCount: number | null = null; + + constructor(project: MapeoProjectApi) { + this.#project = project; + } + + subscribe = (listener: () => void) => { + this.#listeners.add(listener); + if (!this.#isSubscribedInternal) this.#startSubscription(); + return () => { + this.#listeners.delete(listener); + if (this.#listeners.size === 0) this.#stopSubscription(); + }; + }; + + getStateSnapshot = () => { + if (this.#error) throw this.#error; + return this.#state; + }; + + getProgressSnapshot = () => { + if (this.#maxDataSyncCount === null || this.#state === null) { + return null; + } + + if (this.#maxDataSyncCount === 0) { + return 1; + } + + const currentCount = this.#state.data.want + this.#state.data.wanted; + + const ratio = + (this.#maxDataSyncCount - currentCount) / this.#maxDataSyncCount; + + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; - function onSyncState(newState: SyncState) { - state = newState; - error = undefined; - listeners.forEach(listener => listener()); + return clamp(ratio, 0.01, 0.99); + }; + + #notifyListeners() { + for (const listener of this.#listeners) { + listener(); + } } - function subscribeInternal() { - project.$sync.on('sync-state', onSyncState); - isSubscribedInternal = true; - project.$sync + #onSyncState = (state: SyncState) => { + // Indicates whether data syncing went from enabled to disabled + const isDataSyncStopped = this.#state?.data.syncing && !state.data.syncing; + + if (isDataSyncStopped) { + this.#maxDataSyncCount = null; + } else { + const newSyncCount = state.data.want + state.data.wanted; + + this.#maxDataSyncCount = + this.#maxDataSyncCount === null + ? newSyncCount + : Math.max(this.#maxDataSyncCount, newSyncCount); + } + + this.#state = state; + this.#error = null; + this.#notifyListeners(); + }; + + #startSubscription = () => { + this.#project.$sync.on('sync-state', this.#onSyncState); + this.#isSubscribedInternal = true; + this.#project.$sync .getState() - .then(onSyncState) + .then(this.#onSyncState) .catch(e => { - error = e; - listeners.forEach(listener => listener()); + this.#error = e; + this.#notifyListeners(); }); - } - - function unsubscribeInternal() { - isSubscribedInternal = false; - project.$sync.off('sync-state', onSyncState); - } + }; - return { - subscribe: (listener: () => void) => { - listeners.add(listener); - if (!isSubscribedInternal) subscribeInternal(); - return () => { - listeners.delete(listener); - if (listeners.size === 0) unsubscribeInternal(); - }; - }, - getSnapshot: () => { - if (error) throw error; - return state; - }, + #stopSubscription = () => { + this.#isSubscribedInternal = false; + this.#project.$sync.off('sync-state', this.#onSyncState); }; } + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(value, max)); +} + +function identity(state: SyncState | undefined) { + return state; +} diff --git a/src/frontend/hooks/useTabNavigationStore.ts b/src/frontend/hooks/useTabNavigationStore.ts new file mode 100644 index 000000000..3918e2b04 --- /dev/null +++ b/src/frontend/hooks/useTabNavigationStore.ts @@ -0,0 +1,14 @@ +import {create} from 'zustand'; +import {TabName} from '../Navigation/types'; + +type NavigationStoreState = { + currentTab: TabName; + initialRouteName: TabName.Map; + setCurrentTab: (tab: TabName) => void; +}; + +export const useTabNavigationStore = create(set => ({ + initialRouteName: TabName.Map, + currentTab: TabName.Map, + setCurrentTab: (tab: TabName) => set(() => ({currentTab: tab})), +})); diff --git a/src/frontend/images/ObservationsProject.svg b/src/frontend/images/ObservationsProject.svg new file mode 100644 index 000000000..d382244ea --- /dev/null +++ b/src/frontend/images/ObservationsProject.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/frontend/images/StartTracking.svg b/src/frontend/images/StartTracking.svg new file mode 100644 index 000000000..0f52c9c71 --- /dev/null +++ b/src/frontend/images/StartTracking.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/images/StopTracking.svg b/src/frontend/images/StopTracking.svg new file mode 100644 index 000000000..2e5139a24 --- /dev/null +++ b/src/frontend/images/StopTracking.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/images/alert-icon.png b/src/frontend/images/alert-icon.png new file mode 100644 index 000000000..8bcb094e8 Binary files /dev/null and b/src/frontend/images/alert-icon.png differ diff --git a/src/frontend/lib/linking.ts b/src/frontend/lib/linking.ts new file mode 100644 index 000000000..1a1786d60 --- /dev/null +++ b/src/frontend/lib/linking.ts @@ -0,0 +1,12 @@ +import {Platform, Linking} from 'react-native'; + +export async function openWiFiSettings() { + if (Platform.OS !== 'android') + throw new Error( + 'openWiFiSettings() is currently only available on Android', + ); + + // https://github.com/facebook/react-native/blob/v0.73.5/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java#L204 + // https://developer.android.com/reference/android/provider/Settings#ACTION_WIFI_SETTINGS + return Linking.sendIntent('android.settings.WIFI_SETTINGS'); +} diff --git a/src/frontend/metrics/generateMetricsReport.test.ts b/src/frontend/metrics/generateMetricsReport.test.ts new file mode 100644 index 000000000..ff319b655 --- /dev/null +++ b/src/frontend/metrics/generateMetricsReport.test.ts @@ -0,0 +1,105 @@ +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import generateMetricsReport from './generateMetricsReport'; + +describe('generateMetricsReport', () => { + const packageJson = readPackageJson(); + + const defaultOptions: Parameters[0] = { + packageJson, + os: 'android', + osVersion: 123, + screen: {width: 12, height: 34}, + observations: [ + // Middle of the Atlantic + {lat: 10, lon: -33}, + // Mexico City + {lat: 19.419914, lon: -99.088059}, + // Machias Seal Island, disputed territory + {lat: 44.5, lon: -67.101111}, + // To be ignored + {}, + {lat: 12}, + {lon: 34}, + ], + }; + + it('can be serialized and deserialized as JSON', () => { + const report = generateMetricsReport(defaultOptions); + const actual = JSON.parse(JSON.stringify(report)); + const expected = removeUndefinedEntries(report); + expect(actual).toEqual(expected); + }); + + it('includes a report type', () => { + const report = generateMetricsReport(defaultOptions); + expect(report.type).toBe('metrics-v1'); + }); + + it('includes the app version', () => { + const report = generateMetricsReport(defaultOptions); + expect(report.appVersion).toBe(packageJson.version); + }); + + it('includes the OS (Android style)', () => { + const report = generateMetricsReport(defaultOptions); + expect(report.os).toBe('android'); + expect(report.osVersion).toBe(123); + }); + + it('includes the OS (iOS style)', () => { + const options = {...defaultOptions, os: 'ios' as const, osVersion: '1.2.3'}; + const report = generateMetricsReport(options); + expect(report.os).toBe('ios'); + expect(report.osVersion).toBe('1.2.3'); + }); + + it('includes the OS (desktop style)', () => { + const options = { + ...defaultOptions, + os: 'win32' as const, + osVersion: '1.2.3', + }; + const report = generateMetricsReport(options); + expect(report.os).toBe('win32'); + expect(report.osVersion).toBe('1.2.3'); + }); + + it('includes screen dimensions', () => { + const report = generateMetricsReport(defaultOptions); + expect(report.screen).toEqual({width: 12, height: 34}); + }); + + it("doesn't include countries if no observations are provided", () => { + const options = {...defaultOptions, observations: []}; + const report = generateMetricsReport(options); + expect(report.countries).toBe(undefined); + }); + + it('includes countries where observations are found', () => { + const report = generateMetricsReport(defaultOptions); + expect(report.countries).toHaveLength(new Set(report.countries).size); + expect(new Set(report.countries)).toEqual(new Set(['MEX', 'CAN', 'USA'])); + }); +}); + +function readPackageJson() { + const packageJsonPath = path.resolve( + __dirname, + '..', + '..', + '..', + 'package.json', + ); + const packageJsonData = fs.readFileSync(packageJsonPath, 'utf8'); + return JSON.parse(packageJsonData); +} + +function removeUndefinedEntries( + obj: Record, +): Record { + const definedEntries = Object.entries(obj).filter( + entry => entry[1] !== undefined, + ); + return Object.fromEntries(definedEntries); +} diff --git a/src/frontend/metrics/generateMetricsReport.ts b/src/frontend/metrics/generateMetricsReport.ts new file mode 100644 index 000000000..ca440deda --- /dev/null +++ b/src/frontend/metrics/generateMetricsReport.ts @@ -0,0 +1,38 @@ +import type {ReadonlyDeep} from 'type-fest'; +import type {Observation} from '@mapeo/schema'; +import positionToCountries from './positionToCountries'; + +export default function generateMetricsReport({ + packageJson, + os, + osVersion, + screen, + observations, +}: ReadonlyDeep<{ + packageJson: {version: string}; + os: 'android' | 'ios' | NodeJS.Platform; + osVersion: number | string; + screen: {width: number; height: number}; + observations: ReadonlyArray>; +}>) { + const countries = new Set(); + + for (const {lat, lon} of observations) { + if (typeof lat === 'number' && typeof lon === 'number') { + addToSet(countries, positionToCountries(lat, lon)); + } + } + + return { + type: 'metrics-v1', + appVersion: packageJson.version, + os, + osVersion, + screen, + ...(countries.size ? {countries: Array.from(countries)} : {}), + }; +} + +function addToSet(set: Set, toAdd: Iterable): void { + for (const item of toAdd) set.add(item); +} diff --git a/src/frontend/metrics/positionToCountries.test.ts b/src/frontend/metrics/positionToCountries.test.ts new file mode 100644 index 000000000..87fc028b9 --- /dev/null +++ b/src/frontend/metrics/positionToCountries.test.ts @@ -0,0 +1,27 @@ +import positionToCountries from './positionToCountries'; + +describe('positionToCountries', () => { + it('returns nothing for invalid values', () => { + expect(positionToCountries(-91, 181)).toEqual(new Set()); + expect(positionToCountries(Infinity, -Infinity)).toEqual(new Set()); + expect(positionToCountries(NaN, NaN)).toEqual(new Set()); + }); + + it('returns nothing for the middle of the Atlantic ocean', () => { + expect(positionToCountries(10, -33)).toEqual(new Set()); + }); + + it('returns Mexico for a point in Mexico City', () => { + expect(positionToCountries(19.419914, -99.088059)).toEqual( + new Set(['MEX']), + ); + }); + + it('returns multiple countries for disputed territories', () => { + // [Machias Seal Island][0] is a disputed territory. + // [0]: https://en.wikipedia.org/wiki/Machias_Seal_Island + expect(positionToCountries(44.5, -67.101111)).toEqual( + new Set(['CAN', 'USA']), + ); + }); +}); diff --git a/src/frontend/metrics/positionToCountries.ts b/src/frontend/metrics/positionToCountries.ts new file mode 100644 index 000000000..238cc84fa --- /dev/null +++ b/src/frontend/metrics/positionToCountries.ts @@ -0,0 +1,29 @@ +import borders from '@osm_borders/maritime_10000m'; +import GeojsonGeometriesLookup from 'geojson-geometries-lookup'; + +let lookup: undefined | GeojsonGeometriesLookup; + +export default function positionToCountries( + latitude: number, + longitude: number, +): Set { + lookup ??= new GeojsonGeometriesLookup(borders); + + const result = new Set(); + + const {features} = lookup.getContainers({ + type: 'Point', + coordinates: [longitude, latitude], + }); + for (const {properties} of features) { + if ( + properties && + 'isoA3' in properties && + typeof properties.isoA3 === 'string' + ) { + result.add(properties.isoA3); + } + } + + return result; +} diff --git a/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsDisabled.tsx b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsDisabled.tsx new file mode 100644 index 000000000..29b409653 --- /dev/null +++ b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsDisabled.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import {Image, Linking, StyleSheet, View} from 'react-native'; +import {Button} from '../../../sharedComponents/Button'; +import {Text} from '../../../sharedComponents/Text'; +import * as Location from 'expo-location'; +import {defineMessages, useIntl} from 'react-intl'; + +const handleOpenSettings = () => { + Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS'); +}; + +const m = defineMessages({ + gpsDisabledTitle: { + id: 'Modal.GPSDisable.title', + defaultMessage: 'GPS Disabled', + }, + gpsDisabledDescription: { + id: 'Modal.GPSDisable.description', + defaultMessage: + 'To create a Track CoMapeo needs access to your location and GPS.', + }, + gpsDisabledButtonText: { + id: 'Modal.GPSDisable.button', + defaultMessage: 'Enable', + }, +}); + +interface GPSPermissionsDisabled { + setIsGranted: React.Dispatch>; +} +export const GPSPermissionsDisabled: React.FC = ({ + setIsGranted, +}) => { + const {formatMessage} = useIntl(); + const requestForLocationPermissions = async () => { + const [foregroundPermission, backgroundPermission] = await Promise.all([ + Location.requestForegroundPermissionsAsync(), + Location.requestBackgroundPermissionsAsync(), + ]); + if (foregroundPermission.granted && backgroundPermission.granted) { + setIsGranted(true); + } else if ( + !foregroundPermission.canAskAgain || + !backgroundPermission.canAskAgain + ) { + handleOpenSettings(); + } + }; + + return ( + + + + {formatMessage(m.gpsDisabledTitle)} + + {formatMessage(m.gpsDisabledDescription)} + + + + ); +}; + +const styles = StyleSheet.create({ + wrapper: { + padding: 30, + zIndex: 11, + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + }, + image: {marginBottom: 30}, + title: {fontSize: 24, fontWeight: 'bold', textAlign: 'center'}, + description: {fontSize: 20, textAlign: 'center', marginBottom: 30}, + button: {marginBottom: 20, marginVertical: 8.5}, + buttonText: {fontWeight: '500', color: '#fff'}, +}); diff --git a/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsEnabled.tsx b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsEnabled.tsx new file mode 100644 index 000000000..9ef41fd40 --- /dev/null +++ b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsEnabled.tsx @@ -0,0 +1,111 @@ +import React, {useCallback} from 'react'; +import {StyleSheet, View} from 'react-native'; +import {Button} from '../../../sharedComponents/Button'; +import {Text} from '../../../sharedComponents/Text'; +import {useTracking} from '../../../hooks/tracks/useTracking'; +import StartTrackingIcon from '../../../images/StartTracking.svg'; +import StopTrackingIcon from '../../../images/StopTracking.svg'; +import {useTrackTimerContext} from '../../../contexts/TrackTimerContext'; +import {defineMessages, useIntl} from 'react-intl'; + +const m = defineMessages({ + defaultButtonText: { + id: 'Modal.GPSEnable.button.default', + defaultMessage: 'Start Tracks', + }, + stopButtonText: { + id: 'Modal.GPSEnable.button.stop', + defaultMessage: 'Stop Tracks', + }, + loadingButtonText: { + id: 'Modal.GPSEnable.button.loading', + defaultMessage: 'Loading...', + }, + trackingDescription: { + id: 'Modal.GPSEnable.trackingDescription', + defaultMessage: 'You’ve been recording for', + }, +}); + +export const GPSPermissionsEnabled = () => { + const {formatMessage} = useIntl(); + const {isTracking, cancelTracking, startTracking, loading} = useTracking(); + const {timer} = useTrackTimerContext(); + + const styles = getStyles(isTracking); + + const handleTracking = useCallback(() => { + isTracking ? cancelTracking() : startTracking(); + }, [cancelTracking, isTracking, startTracking]); + + const getButtonTitle = () => { + if (loading) return m.loadingButtonText; + if (isTracking) return m.stopButtonText; + return m.defaultButtonText; + }; + + return ( + + + {isTracking && ( + + + + {formatMessage(m.trackingDescription)} + + {timer} + + )} + + ); +}; + +const getStyles = (isTracking: boolean) => { + return StyleSheet.create({ + button: {backgroundColor: isTracking ? '#D92222' : '#0066FF'}, + container: {paddingHorizontal: 20, paddingVertical: 30, height: 140}, + buttonWrapper: { + flexDirection: 'row', + display: 'flex', + alignItems: 'center', + width: '100%', + }, + buttonText: { + fontWeight: '500', + color: '#fff', + width: '100%', + flex: 1, + textAlign: 'center', + }, + runtimeWrapper: { + paddingTop: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + indicator: { + marginRight: 5, + height: 10, + width: 10, + borderRadius: 99, + backgroundColor: '#59A553', + }, + text: {fontSize: 16}, + timer: { + marginLeft: 5, + fontWeight: 'bold', + fontSize: 16, + }, + }); +}; diff --git a/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsModal.tsx b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsModal.tsx new file mode 100644 index 000000000..6a87124a2 --- /dev/null +++ b/src/frontend/screens/MapScreen/GPSPermissions/GPSPermissionsModal.tsx @@ -0,0 +1,59 @@ +import React, {useEffect, useState} from 'react'; +import {GPSPermissionsDisabled} from './GPSPermissionsDisabled'; +import {GPSPermissionsEnabled} from './GPSPermissionsEnabled'; +import * as Location from 'expo-location'; +import {useGPSModalContext} from '../../../contexts/GPSModalContext'; +import {useTabNavigationStore} from '../../../hooks/useTabNavigationStore.ts'; +import {BottomSheetModal, BottomSheetView} from '@gorhom/bottom-sheet'; +import {TAB_BAR_HEIGHT} from '../../../Navigation/ScreenGroups/AppScreens'; +import {StyleSheet} from 'react-native'; +import {TabName} from '../../../Navigation/types'; +import {useFocusEffect} from '@react-navigation/native'; + +export const GPSPermissionsModal = React.memo(() => { + const {setCurrentTab} = useTabNavigationStore(); + const [backgroundStatus] = Location.useBackgroundPermissions(); + const [foregroundStatus] = Location.useForegroundPermissions(); + + const [isGranted, setIsGranted] = useState(null); + const {bottomSheetRef} = useGPSModalContext(); + + useEffect(() => { + if (backgroundStatus && foregroundStatus && isGranted === null) { + setIsGranted(backgroundStatus.granted && foregroundStatus.granted); + } + }, [backgroundStatus, foregroundStatus, isGranted]); + + const onBottomSheetDismiss = () => { + setCurrentTab(TabName.Map); + }; + useFocusEffect(() => { + return () => bottomSheetRef?.current?.close(); + }); + + return ( + null}> + + {isGranted ? ( + + ) : ( + + )} + + + ); +}); + +const styles = StyleSheet.create({ + modal: { + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + minHeight: 140, + }, +}); diff --git a/src/frontend/screens/MapScreen/ObsevationMapLayer.tsx b/src/frontend/screens/MapScreen/ObsevationMapLayer.tsx index d287706b5..98185be5a 100644 --- a/src/frontend/screens/MapScreen/ObsevationMapLayer.tsx +++ b/src/frontend/screens/MapScreen/ObsevationMapLayer.tsx @@ -2,8 +2,8 @@ import {Observation} from '@mapeo/schema'; import React from 'react'; import MapboxGL from '@rnmapbox/maps'; import {useAllObservations} from '../../hooks/useAllObservations'; -import {OnPressEvent} from '@rnmapbox/maps/lib/typescript/types/OnPressEvent'; import {useNavigationFromHomeTabs} from '../../hooks/useNavigationWithTypes'; +import {useCurrentTrackStore} from '../../hooks/tracks/useCurrentTrackStore'; const DEFAULT_MARKER_COLOR = '#F29D4B'; @@ -17,26 +17,28 @@ const layerStyles = { export const ObservationMapLayer = () => { const observations = useAllObservations(); const {navigate} = useNavigationFromHomeTabs(); - + const isTracking = useCurrentTrackStore(state => state.isTracking); const featureCollection: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: mapObservationsToFeatures(observations), }; - function handlePressEvent(event: OnPressEvent) { - const properties = event.features[0].properties; - if (!properties) return; - if (!('id' in properties)) return; - - navigate('Observation', {observationId: properties.id}); - } - return ( { + const properties = event.features[0]?.properties; + if (!properties) return; + if (!('id' in properties)) return; + + navigate('Observation', {observationId: properties.id}); + }} id="observations-source" shape={featureCollection}> - + ); }; diff --git a/src/frontend/screens/MapScreen/UserLocation.tsx b/src/frontend/screens/MapScreen/UserLocation.tsx index a5596b073..60607db30 100644 --- a/src/frontend/screens/MapScreen/UserLocation.tsx +++ b/src/frontend/screens/MapScreen/UserLocation.tsx @@ -1,19 +1,21 @@ -import MapboxGL from '@rnmapbox/maps'; +import {UserLocation as MBUserLocation} from '@rnmapbox/maps'; import * as React from 'react'; -// import {useExperiments} from '../../hooks/useExperiments'; +import {useCurrentTrackStore} from '../../hooks/tracks/useCurrentTrackStore'; +import {useIsFullyFocused} from '../../hooks/useIsFullyFocused'; +import {UserTooltipMarker} from './track/UserTooltipMarker'; + interface UserLocationProps { - visible: boolean; minDisplacement: number; } -export const UserLocation = ({visible, minDisplacement}: UserLocationProps) => { - // const [{directionalArrow}] = useExperiments(); +export const UserLocation = ({minDisplacement}: UserLocationProps) => { + const isTracking = useCurrentTrackStore(state => state.isTracking); + const isFocused = useIsFullyFocused(); return ( - + <> + + {isTracking && } + ); }; diff --git a/src/frontend/screens/MapScreen/index.tsx b/src/frontend/screens/MapScreen/index.tsx index 2ab24bf32..2d80fbd3e 100644 --- a/src/frontend/screens/MapScreen/index.tsx +++ b/src/frontend/screens/MapScreen/index.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; -import Mapbox, {UserLocation} from '@rnmapbox/maps'; +import Mapbox from '@rnmapbox/maps'; import config from '../../../config.json'; import {IconButton} from '../../sharedComponents/IconButton'; import { LocationFollowingIcon, LocationNoFollowIcon, } from '../../sharedComponents/icons'; + import {View, StyleSheet} from 'react-native'; import {ObservationMapLayer} from './ObsevationMapLayer'; import {AddButton} from '../../sharedComponents/AddButton'; @@ -13,10 +14,13 @@ import {useNavigationFromHomeTabs} from '../../hooks/useNavigationWithTypes'; import {useDraftObservation} from '../../hooks/useDraftObservation'; // @ts-ignore import ScaleBar from 'react-native-scale-bar'; -import {getCoords, useLocation} from '../../hooks/useLocation'; -import {useIsFullyFocused} from '../../hooks/useIsFullyFocused'; +import {getCoords} from '../../hooks/useLocation'; import {useLastKnownLocation} from '../../hooks/useLastSavedLocation'; import {useLocationProviderStatus} from '../../hooks/useLocationProviderStatus'; +import {GPSPermissionsModal} from './GPSPermissions/GPSPermissionsModal'; +import {TrackPathLayer} from './track/TrackPathLayer'; +import {UserLocation} from './UserLocation'; +import {useSharedLocationContext} from '../../contexts/SharedLocationContext'; // This is the default zoom used when the map first loads, and also the zoom // that the map will zoom to if the user clicks the "Locate" button and the @@ -24,21 +28,19 @@ import {useLocationProviderStatus} from '../../hooks/useLocationProviderStatus'; const DEFAULT_ZOOM = 12; Mapbox.setAccessToken(config.mapboxAccessToken); -const MIN_DISPLACEMENT = 15; +const MIN_DISPLACEMENT = 3; export const MAP_STYLE = Mapbox.StyleURL.Outdoors; export const MapScreen = () => { const [zoom, setZoom] = React.useState(DEFAULT_ZOOM); - - const isFocused = useIsFullyFocused(); const [isFinishedLoading, setIsFinishedLoading] = React.useState(false); const [following, setFollowing] = React.useState(true); const {newDraft} = useDraftObservation(); const {navigate} = useNavigationFromHomeTabs(); - const {location} = useLocation({maxDistanceInterval: MIN_DISPLACEMENT}); + const {locationState} = useSharedLocationContext(); const savedLocation = useLastKnownLocation(); - const coords = location && getCoords(location); + const coords = locationState.location && getCoords(locationState.location); const locationProviderStatus = useLocationProviderStatus(); const locationServicesEnabled = !!locationProviderStatus?.locationServicesEnabled; @@ -97,21 +99,18 @@ export const MapScreen = () => { followUserLocation={false} /> - {isFinishedLoading && } - {coords !== undefined && locationServicesEnabled && ( - + {coords && locationServicesEnabled && ( + )} + {isFinishedLoading && } + {isFinishedLoading && } - - {coords !== undefined && locationServicesEnabled && ( + {coords && locationServicesEnabled && ( {following ? : } @@ -123,6 +122,7 @@ export const MapScreen = () => { onPress={handleAddPress} isLoading={!isFinishedLoading} /> + ); }; diff --git a/src/frontend/screens/MapScreen/track/TrackPathLayer.tsx b/src/frontend/screens/MapScreen/track/TrackPathLayer.tsx new file mode 100644 index 000000000..da9650783 --- /dev/null +++ b/src/frontend/screens/MapScreen/track/TrackPathLayer.tsx @@ -0,0 +1,58 @@ +import {LineJoin, LineLayer, ShapeSource} from '@rnmapbox/maps'; +import {useCurrentTrackStore} from '../../../hooks/tracks/useCurrentTrackStore'; +import * as React from 'react'; +import {StyleSheet} from 'react-native'; +import {LineString} from 'geojson'; +import {useLocation} from '../../../hooks/useLocation'; +import {LocationHistoryPoint} from '../../../sharedTypes/location'; + +export const TrackPathLayer = () => { + const locationHistory = useCurrentTrackStore(state => state.locationHistory); + const isTracking = useCurrentTrackStore(state => state.isTracking); + const {location} = useLocation({maxDistanceInterval: 3}); + const finalLocationHistory = location?.coords + ? [ + ...locationHistory, + { + latitude: location.coords.latitude, + longitude: location.coords.longitude, + timestamp: new Date().getTime(), + }, + ] + : locationHistory; + + return ( + locationHistory.length > 1 && + isTracking && ( + console.log('display bottom sheet')} + id="routeSource" + shape={toRoute(finalLocationHistory)}> + + + ) + ); +}; + +const toRoute = (locations: LocationHistoryPoint[]): LineString => { + return { + type: 'LineString', + coordinates: locations.map(location => [ + location.longitude, + location.latitude, + ]), + }; +}; + +const styles = StyleSheet.create({ + lineLayer: { + lineColor: '#000000', + lineWidth: 5, + lineCap: LineJoin.Round, + lineOpacity: 1.84, + }, +} as any); diff --git a/src/frontend/screens/MapScreen/track/UserTooltipMarker.tsx b/src/frontend/screens/MapScreen/track/UserTooltipMarker.tsx new file mode 100644 index 000000000..32bbccf0b --- /dev/null +++ b/src/frontend/screens/MapScreen/track/UserTooltipMarker.tsx @@ -0,0 +1,84 @@ +import {MarkerView} from '@rnmapbox/maps'; +import {StyleSheet, Text, View} from 'react-native'; +import React from 'react'; +import {useCurrentTrackStore} from '../../../hooks/tracks/useCurrentTrackStore'; +import {useTrackTimerContext} from '../../../contexts/TrackTimerContext'; +import {useSharedLocationContext} from '../../../contexts/SharedLocationContext'; + +export const UserTooltipMarker = () => { + const {timer} = useTrackTimerContext(); + const {locationState} = useSharedLocationContext(); + const totalDistance = useCurrentTrackStore(state => state.distance); + + return ( + locationState.location?.coords && ( + + + + + {totalDistance.toFixed(2)}km + + + + {timer} + + + + + + + ) + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: 13, + }, + wrapper: { + backgroundColor: '#FFF', + padding: 10, + borderRadius: 5, + alignItems: 'center', + justifyContent: 'center', + color: 'black', + display: 'flex', + flexDirection: 'row', + }, + text: { + color: '#333333', + }, + separator: { + marginLeft: 10, + marginRight: 10, + height: 12, + borderColor: '#CCCCD6', + borderLeftWidth: 1, + color: '#CCCCD6', + }, + indicator: { + marginLeft: 5, + height: 10, + width: 10, + borderRadius: 99, + backgroundColor: '#59A553', + }, + arrow: { + alignItems: 'center', + justifyContent: 'center', + borderTopWidth: 15, + borderLeftWidth: 10, + borderRightWidth: 10, + borderTopColor: '#FFF', + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, +}); diff --git a/src/frontend/screens/ObservationEdit/SaveButton.tsx b/src/frontend/screens/ObservationEdit/SaveButton.tsx index c3632111f..5b4a2f724 100644 --- a/src/frontend/screens/ObservationEdit/SaveButton.tsx +++ b/src/frontend/screens/ObservationEdit/SaveButton.tsx @@ -13,6 +13,7 @@ import {UIActivityIndicator} from 'react-native-indicators'; import {useCreateBlobMutation} from '../../hooks/server/media'; import {DraftPhoto, Photo} from '../../contexts/PhotoPromiseContext/types'; import {useDraftObservation} from '../../hooks/useDraftObservation'; +import {useCurrentTrackStore} from '../../hooks/tracks/useCurrentTrackStore'; const m = defineMessages({ noGpsTitle: { @@ -73,6 +74,12 @@ export const SaveButton = ({ const createObservationMutation = useCreateObservation(); const editObservationMutation = useEditObservation(); const createBlobMutation = useCreateBlobMutation(); + const addNewTrackLocation = useCurrentTrackStore( + state => state.addNewLocations, + ); + const addNewTrackObservation = useCurrentTrackStore( + state => state.addNewObservation, + ); function createObservation() { if (!value) throw new Error('no observation saved in persisted state '); @@ -127,9 +134,21 @@ export const SaveButton = ({ onError: () => { if (openErrorModal) openErrorModal(); }, - onSuccess: () => { + onSuccess: data => { clearDraft(); navigation.navigate('Home', {screen: 'Map'}); + if (value.lat && value.lon) { + addNewTrackLocation([ + { + timestamp: new Date().getTime(), + latitude: value.lat, + longitude: value.lon, + }, + ]); + } + if (data.docId) { + addNewTrackObservation(data.docId); + } }, }, ); @@ -151,6 +170,16 @@ export const SaveButton = ({ onSuccess: () => { clearDraft(); navigation.pop(); + if (value.lat && value.lon) { + addNewTrackLocation([ + { + timestamp: new Date().getTime(), + latitude: value.lat, + longitude: value.lon, + }, + ]); + } + addNewTrackObservation(observationId); }, }, ); diff --git a/src/frontend/screens/ObservationsList/ObservationListItem.tsx b/src/frontend/screens/ObservationsList/ObservationListItem.tsx index 81a4fa4e9..26eee8c50 100644 --- a/src/frontend/screens/ObservationsList/ObservationListItem.tsx +++ b/src/frontend/screens/ObservationsList/ObservationListItem.tsx @@ -4,10 +4,7 @@ import {Text} from '../../sharedComponents/Text'; import {TouchableHighlight} from '../../sharedComponents/Touchables'; import {CategoryCircleIcon} from '../../sharedComponents/icons/CategoryIcon'; -//import PhotoView from "../../sharedComponents/PhotoView"; -// import useDeviceId from "../../hooks/useDeviceId"; import {Attachment, ViewStyleProp} from '../../sharedTypes'; -import {filterPhotosFromAttachments} from '../../hooks/persistedState/usePersistedDraftObservation/photosMethods'; import {BLACK} from '../../lib/styles'; import {Observation} from '@mapeo/schema'; import { diff --git a/src/frontend/screens/Settings/CreateOrJoinProject/CreateProject/ProjectCreated.tsx b/src/frontend/screens/Settings/CreateOrJoinProject/CreateProject/ProjectCreated.tsx index fc35536c3..58b5ff3f0 100644 --- a/src/frontend/screens/Settings/CreateOrJoinProject/CreateProject/ProjectCreated.tsx +++ b/src/frontend/screens/Settings/CreateOrJoinProject/CreateProject/ProjectCreated.tsx @@ -49,6 +49,24 @@ export const ProjectCreated = ({ ); } + //This resets the navigation so the user cannot press back and return to this screen + function handleGoToInviteScreen() { + navigation.dispatch(state => { + const index = state.routes.findIndex(r => r.name === 'Settings'); + const routes = [ + ...state.routes.slice(0, index + 1), + {name: 'YourTeam'}, + {name: 'SelectDevice'}, + ]; + + return CommonActions.reset({ + ...state, + routes, + index: routes.length - 1, + }); + }); + } + return ( @@ -64,7 +82,7 @@ export const ProjectCreated = ({ - + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 20, + paddingTop: 80, + alignItems: 'center', + justifyContent: 'space-between', + flex: 1, + }, +}); diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx new file mode 100644 index 000000000..e3531c1ac --- /dev/null +++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import {StyleSheet, View} from 'react-native'; +import {Button} from '../../../../../sharedComponents/Button'; +import ErrorIcon from '../../../../../images/Error.svg'; +import {defineMessages, useIntl} from 'react-intl'; +import {Text} from '../../../../../sharedComponents/Text'; +import {DeviceNameWithIcon} from '../../../../../sharedComponents/DeviceNameWithIcon'; +import {RoleWithIcon} from '../../../../../sharedComponents/RoleWithIcon'; +import { + COORDINATOR_ROLE_ID, + NativeRootNavigationProps, +} from '../../../../../sharedTypes'; +import {useProjectSettings} from '../../../../../hooks/server/projects'; + +const m = defineMessages({ + unableToCancel: { + id: 'screens.Settings.YourTeam.unableToCancel', + defaultMessage: 'Unable to Cancel Invitation', + }, + deviceHasJoined: { + id: 'screens.Settings.YourTeam.deviceHasJoined', + defaultMessage: 'Device Has Joined {projectName}', + }, + close: { + id: 'screens.Settings.YourTeam.close', + defaultMessage: 'Close', + }, +}); + +export const UnableToCancelInvite = ({ + navigation, + route, +}: NativeRootNavigationProps<'UnableToCancelInvite'>) => { + const {formatMessage} = useIntl(); + const {role, ...deviceInfo} = route.params; + const {data} = useProjectSettings(); + + return ( + + + + + {formatMessage(m.unableToCancel)} + + {data?.name && ( + + {formatMessage(m.deviceHasJoined, {projectName: data.name})} + + )} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 20, + paddingTop: 80, + alignItems: 'center', + justifyContent: 'space-between', + flex: 1, + }, +}); diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx index 2db3322af..a8f7745fb 100644 --- a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx +++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx @@ -22,7 +22,11 @@ const m = defineMessages({ }, }); -export const WaitingForInviteAccept = () => { +export const WaitingForInviteAccept = ({ + cancelInvite, +}: { + cancelInvite: () => void; +}) => { const {formatMessage: t} = useIntl(); const [time, setTime] = React.useState(0); const navigation = useNavigationFromRoot(); @@ -53,12 +57,7 @@ export const WaitingForInviteAccept = () => { {t(m.waitingMessage)} {t(m.timerMessage, {seconds: time})} - { - navigation.navigate('YourTeam'); - }} - /> + ); }; diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx index 7d112ea5c..0a4230183 100644 --- a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx +++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import {NativeNavigationComponent} from '../../../../../sharedTypes'; import {defineMessages} from 'react-intl'; import {useBottomSheetModal} from '../../../../../sharedComponents/BottomSheetModal'; -import {useQueryClient} from '@tanstack/react-query'; -import {useProject} from '../../../../../hooks/server/projects'; import {ErrorModal} from '../../../../../sharedComponents/ErrorModal'; import {ReviewInvitation} from './ReviewInvitation'; import {WaitingForInviteAccept} from './WaitingForInviteAccept'; +import { + useRequestCancelInvite, + useSendInvite, +} from '../../../../../hooks/server/invites'; const m = defineMessages({ title: { @@ -19,36 +21,55 @@ export const ReviewAndInvite: NativeNavigationComponent<'ReviewAndInvite'> = ({ route, navigation, }) => { - const [inviteStatus, setInviteStatus] = React.useState< - 'reviewing' | 'waiting' - >('reviewing'); const {role, deviceId, deviceType, name} = route.params; const {openSheet, sheetRef, closeSheet, isOpen} = useBottomSheetModal({ openOnMount: false, }); - const project = useProject(); - const queryClient = useQueryClient(); + const sendInviteMutation = useSendInvite(); + const requestCancelInviteMutation = useRequestCancelInvite(); function sendInvite() { - setInviteStatus('waiting'); - project.$member - .invite(deviceId, {roleId: role}) - .then(val => { - if (val === 'ACCEPT') { - queryClient.invalidateQueries({queryKey: ['projectMembers']}); - navigation.navigate('InviteAccepted', {...route.params}); - return; - } - }) - .catch(err => { + sendInviteMutation.mutate( + {deviceId, role: {roleId: role}}, + { + onSuccess: val => { + // If user has attempted to cancel an invite, but an invite has already been accepted, let user know their cancellation was unsuccessful + if (val === 'ACCEPT' && requestCancelInviteMutation.isPending) { + navigation.navigate('UnableToCancelInvite', {...route.params}); + return; + } + if (val === 'ACCEPT') { + navigation.navigate('InviteAccepted', route.params); + return; + } + + if (val === 'REJECT') { + navigation.navigate('InviteDeclined', route.params); + return; + } + }, + onError: () => { + openSheet(); + }, + }, + ); + } + + function cancelInvite() { + requestCancelInviteMutation.mutate(deviceId, { + onSuccess: () => { + navigation.navigate('YourTeam'); + }, + onError: () => { openSheet(); - }); + }, + }); } return ( - {inviteStatus === 'reviewing' ? ( + {sendInviteMutation.isIdle ? ( = ({ role={role} /> ) : ( - + )} void; +}) => { + const {formatMessage: t} = useIntl(); + + return ( + + {t(m.buttonText)} + + }> + + {t(m.description)} + + + ); +}; + +const styles = StyleSheet.create({ + contentContainer: {paddingTop: 40}, + descriptionText: { + textAlign: 'center', + fontSize: 40, + }, + buttonText: { + color: WHITE, + fontWeight: 'bold', + fontSize: 20, + }, +}); diff --git a/src/frontend/screens/Sync/HeaderTitle.tsx b/src/frontend/screens/Sync/HeaderTitle.tsx new file mode 100644 index 000000000..b7425c4bf --- /dev/null +++ b/src/frontend/screens/Sync/HeaderTitle.tsx @@ -0,0 +1,60 @@ +import {StyleSheet, View} from 'react-native'; +import {useDimensions} from '@react-native-community/hooks'; +import {defineMessages, useIntl} from 'react-intl'; + +import {useLocalDiscoveryState} from '../../hooks/useLocalDiscoveryState'; +import {Circle} from '../../sharedComponents/icons/Circle'; +import {WifiIcon, WifiOffIcon} from '../../sharedComponents/icons'; +import {Text} from '../../sharedComponents/Text'; +import {COMAPEO_DARK_BLUE, WHITE} from '../../lib/styles'; + +const m = defineMessages({ + noWiFi: { + id: 'screens.Sync.HeaderTitle.noWiFi', + defaultMessage: 'No WiFi', + }, +}); + +export function HeaderTitle() { + const {formatMessage: t} = useIntl(); + const screenWidth = useDimensions().screen.width; + const ssid = useLocalDiscoveryState(state => state.ssid); + + const WifiIconComponent = ssid ? WifiIcon : WifiOffIcon; + + return ( + + + + + + {ssid || t(m.noWiFi)} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + columnGap: 10, + }, + signalIndicator: { + elevation: 0, + backgroundColor: COMAPEO_DARK_BLUE, + }, + wifiText: { + fontWeight: 'bold', + }, +}); diff --git a/src/frontend/screens/Sync/NoWifiDisplay.tsx b/src/frontend/screens/Sync/NoWifiDisplay.tsx new file mode 100644 index 000000000..246d07363 --- /dev/null +++ b/src/frontend/screens/Sync/NoWifiDisplay.tsx @@ -0,0 +1,66 @@ +import {StyleSheet, View} from 'react-native'; +import {defineMessages, useIntl} from 'react-intl'; + +import {WHITE} from '../../lib/styles'; +import {Text} from '../../sharedComponents/Text'; +import {Button} from '../../sharedComponents/Button'; +import {ScreenContentWithDock} from '../../sharedComponents/ScreenContentWithDock'; + +const m = defineMessages({ + title: { + id: 'screens.Sync.NoWifiDisplay.title', + defaultMessage: 'No WiFi', + }, + description: { + id: 'screens.Sync.NoWifiDisplay.description', + defaultMessage: + 'Open your phone settings and connect to a WiFi network to synchronize', + }, + buttonText: { + id: 'screens.Sync.NoWifiDisplay.buttonText', + defaultMessage: 'Open Settings', + }, +}); + +export const NoWifiDisplay = ({ + onOpenSettings, +}: { + onOpenSettings: () => void; +}) => { + const {formatMessage: t} = useIntl(); + + return ( + + {t(m.buttonText)} + + }> + + {t(m.title)} + {t(m.description)} + + + ); +}; + +const styles = StyleSheet.create({ + contentContainer: { + paddingTop: 40, + rowGap: 12, + }, + titleText: { + fontSize: 40, + fontWeight: 'bold', + textAlign: 'center', + }, + descriptionText: { + fontSize: 24, + textAlign: 'center', + }, + buttonText: { + color: WHITE, + fontWeight: 'bold', + fontSize: 20, + }, +}); diff --git a/src/frontend/screens/Sync/ProjectSyncDisplay.tsx b/src/frontend/screens/Sync/ProjectSyncDisplay.tsx new file mode 100644 index 000000000..770924676 --- /dev/null +++ b/src/frontend/screens/Sync/ProjectSyncDisplay.tsx @@ -0,0 +1,274 @@ +import * as React from 'react'; +import {defineMessages, useIntl} from 'react-intl'; +import {StyleSheet, View} from 'react-native'; +import {Bar as ProgressBar} from 'react-native-progress'; +import {useProject} from '../../hooks/server/projects'; +import {SyncState, useSyncProgress} from '../../hooks/useSyncState'; +import ObservationsProjectImage from '../../images/ObservationsProject.svg'; +import { + BLACK, + COMAPEO_BLUE, + DARK_GREY, + LIGHT_GREY, + MEDIUM_GREY, + WHITE, +} from '../../lib/styles'; +import {ScreenContentWithDock} from '../../sharedComponents/ScreenContentWithDock'; +import {Button} from '../../sharedComponents/Button'; +import {Text} from '../../sharedComponents/Text'; +import {StopIcon, SyncIcon, WifiIcon} from '../../sharedComponents/icons'; +import {useQueryClient} from '@tanstack/react-query'; +import {OBSERVATION_KEY} from '../../hooks/server/observations'; +import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes'; + +const m = defineMessages({ + deviceName: { + id: 'screens.Sync.ProjectSyncDisplay.deviceName', + defaultMessage: 'Your device name is {name}', + }, + devicesNearby: { + id: 'screens.Sync.ProjectSyncDisplay.devicesNearby', + defaultMessage: + '{count} {count, plural, one {device} other {devices}} nearby/connected', + }, + buttonTextSync: { + id: 'screens.Sync.ProjectSyncDisplay.buttonTextSync', + defaultMessage: 'Sync', + }, + buttonTextStop: { + id: 'screens.Sync.ProjectSyncDisplay.buttonTextStop', + defaultMessage: 'Stop', + }, + buttonTextDone: { + id: 'screens.Sync.ProjectSyncDisplay.buttonTextDone', + defaultMessage: "You're all caught up", + }, + noDevicesSyncing: { + id: 'screens.Sync.ProjectSyncDisplay.noDevicesSyncing', + defaultMessage: 'No Devices are Syncing', + }, + devicesWaitingToSync: { + id: 'screens.Sync.ProjectSyncDisplay.devicesWaitingToSync', + defaultMessage: + '{count} {count, plural, one {Device} other {Devices}} Waiting to Sync with you', + }, + devicesSyncing: { + id: 'screens.Sync.ProjectSyncDisplay.devicesSyncing', + defaultMessage: + 'Syncing with {count} {count, plural, one {Device} other {Devices}}', + }, + upToDate: { + id: 'screens.Sync.ProjectSyncDisplay.upToDate', + defaultMessage: 'Up to Date!\nNo data to Sync', + }, + syncing: { + id: 'screens.Sync.ProjectSyncDisplay.syncing', + defaultMessage: 'Syncing…', + }, + syncProgress: { + id: 'screens.Sync.ProjectSyncDisplay.syncProgress', + defaultMessage: '{value}%', + }, +}); + +export const ProjectSyncDisplay = ({ + syncState, + projectName, + deviceName, +}: { + syncState: SyncState; + projectName: string; + deviceName: string; +}) => { + const {formatMessage: t} = useIntl(); + + const project = useProject(); + const queryClient = useQueryClient(); + const navigation = useNavigationFromRoot(); + const {connectedPeers, data, initial} = syncState; + const isSyncDone = !initial.dataToSync && !data.dataToSync; + + // stops sync when user leaves sync screen. The api allows us to continue syncing even if the user is not on the sync screen, but for simplicity we are only allowing sync while on the sync screen. In the future we can easily enable background sync, there are just some UI questions that need to answered before we do that. + React.useEffect(() => { + const unsubscribe = navigation.addListener('beforeRemove', () => { + project.$sync.stop(); + queryClient.invalidateQueries({queryKey: [OBSERVATION_KEY]}); + }); + + return unsubscribe; + }, [navigation, project, queryClient]); + + const isDataSyncEnabled = data.syncing; + + const devicesSyncingText = isSyncDone + ? t(m.upToDate) + : !isDataSyncEnabled && connectedPeers === 0 + ? t(m.noDevicesSyncing) + : t( + !isDataSyncEnabled && connectedPeers > 0 + ? m.devicesWaitingToSync + : m.devicesSyncing, + {count: connectedPeers}, + ); + + return ( + { + project.$sync.stop(); + }}> + + + + {t(m.buttonTextStop)} + + + + ) : ( + + ) + }> + + + {projectName && {projectName}} + {deviceName && ( + + {t(m.deviceName, {name: deviceName})} + + )} + + + {t(m.devicesNearby, {count: connectedPeers})} + + + {devicesSyncingText} + {!isSyncDone && isDataSyncEnabled && } + + ); +}; + +function SyncProgress() { + const {formatMessage: t} = useIntl(); + + const progress = useSyncProgress(); + + const dynamicProgressBarProps = + progress === null + ? {indeterminate: true, indeterminateAnimationDuration: 2000} + : { + progress, + indeterminate: false, + }; + + return ( + + + + {t(m.syncing)} + + + + {progress !== null && ( + + {t(m.syncProgress, {value: Math.round(progress * 100)})} + + )} + + ); +} + +const styles = StyleSheet.create({ + contentContainer: { + gap: 36, + }, + projectInfoContainer: { + alignItems: 'center', + gap: 8, + }, + connectedDevicesInfo: { + flexDirection: 'row', + gap: 8, + }, + projectName: { + fontSize: 24, + fontWeight: 'bold', + }, + deviceName: { + color: MEDIUM_GREY, + }, + syncInfo: { + gap: 20, + }, + titleText: { + fontSize: 40, + textAlign: 'center', + }, + descriptionText: { + fontSize: 24, + textAlign: 'center', + }, + buttonContentContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + buttonTextPrimary: { + fontWeight: 'bold', + fontSize: 20, + color: WHITE, + }, + buttonTextSecondary: { + fontWeight: 'bold', + fontSize: 20, + }, + syncProgressContainer: { + gap: 12, + }, + syncProgressTextContainer: { + flexDirection: 'row', + gap: 8, + alignItems: 'center', + }, + syncProgressTitleText: { + fontSize: 20, + color: COMAPEO_BLUE, + }, + syncProgressText: { + color: MEDIUM_GREY, + alignSelf: 'flex-end', + }, +}); diff --git a/src/frontend/screens/Sync/index.tsx b/src/frontend/screens/Sync/index.tsx new file mode 100644 index 000000000..870d537ce --- /dev/null +++ b/src/frontend/screens/Sync/index.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; + +import {NativeRootNavigationProps} from '../../sharedTypes'; +import {IconButton} from '../../sharedComponents/IconButton'; +import {SettingsIcon} from '../../sharedComponents/icons'; +import {useAllProjects, useProjectSettings} from '../../hooks/server/projects'; +import {useLocalDiscoveryState} from '../../hooks/useLocalDiscoveryState'; +import {CreateOrJoinProjectDisplay} from './CreateOrJoinProjectDisplay'; +import {HeaderTitle} from './HeaderTitle'; +import {NoWifiDisplay} from './NoWifiDisplay'; +import {openWiFiSettings} from '../../lib/linking'; +import {ProjectSyncDisplay} from './ProjectSyncDisplay'; +import {Loading} from '../../sharedComponents/Loading'; +import {useSyncState} from '../../hooks/useSyncState'; +import {useDeviceInfo} from '../../hooks/server/deviceInfo'; + +export function createNavigationOptions() { + return ({ + navigation, + }: NativeRootNavigationProps<'Sync'>): NativeStackNavigationOptions => { + return { + headerTitleAlign: 'center', + headerTitle: () => , + headerRight: () => ( + { + navigation.navigate('ProjectSettings'); + }}> + + + ), + }; + }; +} + +export const SyncScreen = ({navigation}: NativeRootNavigationProps<'Sync'>) => { + const wifiStatus = useLocalDiscoveryState(state => state.wifiStatus); + + // TODO: Handle error case + const {isLoading, data} = useAllProjects(); + const syncState = useSyncState(); + const deviceInfoQuery = useDeviceInfo(); + const projectSettingsQuery = useProjectSettings(); + + if (wifiStatus === 'off') { + return ( + { + openWiFiSettings().catch(err => { + // Should not throw but in case it does, no-op + console.warn(err); + }); + }} + /> + ); + } + + // TODO: Replace with proper check of being a part of a shared project + if (data && data.length === 1) { + return ( + navigation.navigate('CreateOrJoinProject')} + /> + ); + } + + if ( + isLoading || + !syncState || + !projectSettingsQuery.data || + !deviceInfoQuery.data + ) { + return ; + } + + return ( + + ); +}; diff --git a/src/frontend/sharedComponents/GPSPill.tsx b/src/frontend/sharedComponents/GPSPill.tsx new file mode 100644 index 000000000..3296e7420 --- /dev/null +++ b/src/frontend/sharedComponents/GPSPill.tsx @@ -0,0 +1,98 @@ +import React, {FC, useMemo} from 'react'; +import {StyleSheet, TouchableOpacity, View} from 'react-native'; +import {Text} from './Text'; +import {ParamListBase, useIsFocused} from '@react-navigation/native'; +import {useLocationProviderStatus} from '../hooks/useLocationProviderStatus'; +import {getLocationStatus} from '../lib/utils'; +import {defineMessages, useIntl} from 'react-intl'; +import {GpsIcon} from './icons'; +import {useSharedLocationContext} from '../contexts/SharedLocationContext'; +import {BLACK, WHITE} from '../lib/styles'; +import {BottomTabNavigationProp} from '@react-navigation/bottom-tabs'; + +const m = defineMessages({ + noGps: { + id: 'sharedComponents.GpsPill.noGps', + defaultMessage: 'No GPS', + }, + searching: { + id: 'sharedComponents.GpsPill.searching', + defaultMessage: 'Searching…', + }, +}); + +interface GPSPill { + navigation: BottomTabNavigationProp; +} + +export const GPSPill: FC = ({navigation}) => { + const isFocused = useIsFocused(); + const {formatMessage: t} = useIntl(); + const {locationState, fgPermissions} = useSharedLocationContext(); + const locationProviderStatus = useLocationProviderStatus(); + + const precision = locationState?.location?.coords.accuracy; + + const status = useMemo(() => { + const isError = !!locationState.error || !fgPermissions; + + return isError + ? 'error' + : getLocationStatus({ + location: locationState.location, + providerStatus: locationProviderStatus, + }); + }, [ + locationProviderStatus, + locationState.error, + locationState.location, + fgPermissions, + ]); + + const text = useMemo(() => { + if (status === 'error') return t(m.noGps); + else if (status === 'searching' || typeof precision === 'undefined') { + return t(m.searching); + } else return `± ${Math.round(precision!)} m`; + }, [precision, status, t]); + + return ( + navigation.navigate('GpsModal')} + testID="gpsPillButton"> + + + {isFocused && } + + + {text} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 0, + minWidth: 100, + maxWidth: 200, + borderRadius: 18, + height: 36, + paddingLeft: 32, + paddingRight: 20, + borderWidth: 3, + borderColor: '#33333366', + backgroundColor: BLACK, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row', + }, + error: {backgroundColor: '#FF0000'}, + text: {color: WHITE}, + icon: {position: 'absolute', left: 6}, +}); diff --git a/src/frontend/sharedComponents/GpsPill.tsx b/src/frontend/sharedComponents/GpsPill.tsx deleted file mode 100644 index 0b35f973f..000000000 --- a/src/frontend/sharedComponents/GpsPill.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from 'react'; -import {View, StyleSheet} from 'react-native'; -import {defineMessages, useIntl} from 'react-intl'; -import {useIsFocused} from '@react-navigation/native'; -import {TouchableOpacity} from 'react-native-gesture-handler'; - -import {BLACK, WHITE} from '../lib/styles'; -import type {LocationStatus} from '../lib/utils'; -import {Text} from './Text'; -import {GpsIcon} from './icons'; - -const m = defineMessages({ - noGps: { - id: 'sharedComponents.GpsPill.noGps', - defaultMessage: 'No GPS', - }, - searching: { - id: 'sharedComponents.GpsPill.searching', - defaultMessage: 'Searching…', - }, -}); - -interface Props { - onPress?: () => void; - precision?: number; - variant: LocationStatus; -} - -export const GpsPill = React.memo( - ({onPress, variant, precision}: Props) => { - const isFocused = useIsFocused(); - const {formatMessage: t} = useIntl(); - let text: string; - if (variant === 'error') text = t(m.noGps); - else if (variant === 'searching' || typeof precision === 'undefined') - text = t(m.searching); - else text = `± ${precision} m`; - return ( - - - - {isFocused && } - - - {text} - - - - ); - }, -); - -const styles = StyleSheet.create({ - container: { - flex: 0, - minWidth: 100, - maxWidth: 200, - borderRadius: 18, - height: 36, - paddingLeft: 32, - paddingRight: 20, - borderWidth: 3, - borderColor: '#33333366', - backgroundColor: BLACK, - justifyContent: 'center', - alignItems: 'center', - flexDirection: 'row', - }, - error: {backgroundColor: '#FF0000'}, - text: {color: WHITE}, - icon: {position: 'absolute', left: 6}, -}); diff --git a/src/frontend/sharedComponents/HomeHeader.tsx b/src/frontend/sharedComponents/HomeHeader.tsx index 428813467..8042238b2 100644 --- a/src/frontend/sharedComponents/HomeHeader.tsx +++ b/src/frontend/sharedComponents/HomeHeader.tsx @@ -1,39 +1,28 @@ -import React from 'react'; +import React, {FC} from 'react'; import {View, StyleSheet} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import {IconButton} from './IconButton'; -import {ObservationListIcon} from './icons'; -import {useNavigationFromHomeTabs} from '../hooks/useNavigationWithTypes'; -import {GpsPill} from './GpsPill'; -import {LocationStatus} from '../lib/utils'; - -interface Props { - locationStatus: LocationStatus; - precision?: number; -} - -export const HomeHeader = ({locationStatus, precision}: Props) => { - const navigation = useNavigationFromHomeTabs(); +import {ObservationListIcon, SyncIconCircle} from './icons'; +import {GPSPill} from './GPSPill'; +import {BottomTabHeaderProps} from '@react-navigation/bottom-tabs'; +export const HomeHeader: FC = ({navigation}) => { return ( - {/* Placeholder for left button */} - { - navigation.navigate('GpsModal'); - }} - /> { - navigation.navigate('ObservationList'); - }} + navigation.navigate('Sync'); + }}> + + + + navigation.navigate('ObservationList')} testID="observationListButton"> @@ -48,7 +37,6 @@ const styles = StyleSheet.create({ alignItems: 'center', backgroundColor: 'transparent', }, - rightButton: {}, leftButton: { width: 60, height: 60, diff --git a/src/frontend/sharedComponents/ProjectInviteBottomSheet.tsx b/src/frontend/sharedComponents/ProjectInviteBottomSheet.tsx index 5552675df..336ee0da6 100644 --- a/src/frontend/sharedComponents/ProjectInviteBottomSheet.tsx +++ b/src/frontend/sharedComponents/ProjectInviteBottomSheet.tsx @@ -11,7 +11,7 @@ import {LIGHT_GREY} from '../lib/styles'; import {View} from 'react-native'; import {useProjectInvite} from '../hooks/useProjectInvite'; import {useNavigationFromRoot} from '../hooks/useNavigationWithTypes'; -import {useNavigationState} from '@react-navigation/native'; +import {CommonActions, useNavigationState} from '@react-navigation/native'; import {isEditingScreen} from '../lib/utils'; const m = defineMessages({ @@ -43,6 +43,10 @@ const m = defineMessages({ id: 'sharedComponents.ProjectInviteBottomSheet.youHaveJoined', defaultMessage: 'You have joined {projName}', }, + goToSync: { + id: 'sharedComponents.ProjectInviteBottomSheet.goToSync', + defaultMessage: 'Go To Sync', + }, }); export const ProjectInviteBottomSheet = () => { @@ -62,6 +66,15 @@ export const ProjectInviteBottomSheet = () => { openSheet(); } + function handleGoToSync() { + navigation.dispatch( + CommonActions.reset({ + index: 1, + routes: [{name: 'Home'}, {name: 'Sync'}], + }), + ); + } + return ( {accept.isSuccess ? ( @@ -76,6 +89,14 @@ export const ProjectInviteBottomSheet = () => { }, text: formatMessage(m.goToMap), }, + { + onPress: () => { + handleGoToSync(); + closeSheet(); + }, + text: formatMessage(m.goToSync), + variation: 'filled', + }, ]} title={formatMessage(m.success)} description={formatMessage(m.youHaveJoined, { diff --git a/src/frontend/sharedComponents/ScreenContentWithDock.tsx b/src/frontend/sharedComponents/ScreenContentWithDock.tsx new file mode 100644 index 000000000..fe6c766f7 --- /dev/null +++ b/src/frontend/sharedComponents/ScreenContentWithDock.tsx @@ -0,0 +1,43 @@ +import {ReactNode, type PropsWithChildren} from 'react'; +import {ScrollView, StyleSheet, View} from 'react-native'; + +import {ViewStyleProp} from '../sharedTypes'; + +export const ScreenContentWithDock = ({ + children, + contentContainerStyle, + dockContainerStyle, + dockContent, +}: PropsWithChildren<{ + dockContent: ReactNode; + dockContainerStyle?: ViewStyleProp; + contentContainerStyle?: ViewStyleProp; +}>) => { + return ( + + + {children} + + + {dockContent} + + + ); +}; + +const styles = StyleSheet.create({ + container: {flex: 1}, + scrollViewRoot: {flex: 1}, + scrollViewContentContainer: { + padding: 20, + }, + dockedContentContainer: { + flex: 0, + padding: 20, + }, +}); diff --git a/src/frontend/sharedComponents/icons/index.tsx b/src/frontend/sharedComponents/icons/index.tsx index 59338bd6d..636955b85 100644 --- a/src/frontend/sharedComponents/icons/index.tsx +++ b/src/frontend/sharedComponents/icons/index.tsx @@ -5,7 +5,7 @@ import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {Image} from 'react-native'; import {Circle} from './Circle'; -import {RED, DARK_GREY, MANGO, MEDIUM_GREY} from '../../lib/styles'; +import {RED, DARK_GREY, MANGO, MEDIUM_GREY, WHITE} from '../../lib/styles'; import type {TextStyleProp, ImageStyleProp} from '../../sharedTypes'; type FontIconProps = { @@ -136,15 +136,11 @@ export const WifiIcon = ({ }: FontIconProps) => ( ); -export const SyncIcon = ({ - size = 30, - color = 'white', - style, -}: FontIconProps) => ( +export const SyncIcon = ({size = 20, color = WHITE}: FontIconProps) => ( ); @@ -198,3 +194,7 @@ export const LocationFollowingIcon = ({ ); + +export const StopIcon = ({color = WHITE, size = 30}: FontIconProps) => ( + +); diff --git a/src/frontend/sharedTypes.ts b/src/frontend/sharedTypes.ts index e30d64804..ad4368a96 100644 --- a/src/frontend/sharedTypes.ts +++ b/src/frontend/sharedTypes.ts @@ -8,7 +8,7 @@ import {MessageDescriptor} from 'react-intl'; import {AppStackList} from './Navigation/AppStack'; import {HomeTabsList} from './Navigation/ScreenGroups/AppScreens'; import {Observation, ObservationValue} from '@mapeo/schema'; -import {type RoleId} from '@mapeo/core/dist/roles'; +import type {RoleId, RoleIdForNewInvite} from '@mapeo/core/dist/roles'; export type ViewStyleProp = StyleProp; export type TextStyleProp = StyleProp; @@ -54,6 +54,7 @@ export type CoordinateFormat = 'utm' | 'dd' | 'dms'; export type DeviceType = 'mobile' | 'desktop'; export type DeviceRole = RoleId; +export type DeviceRoleForNewInvite = RoleIdForNewInvite; // Copied form /@mapeo/core/src/roles.js. Created an issue to eventuall expose this: https://github.com/digidem/mapeo-core-next/issues/532 export const CREATOR_ROLE_ID = 'a12a6702b93bd7ff'; diff --git a/src/frontend/sharedTypes/location.ts b/src/frontend/sharedTypes/location.ts new file mode 100644 index 000000000..b3d800ff2 --- /dev/null +++ b/src/frontend/sharedTypes/location.ts @@ -0,0 +1,21 @@ +export type FullLocationData = { + coords: { + altitude: number; + altitudeAccuracy: number; + latitude: number; + accuracy: number; + longitude: number; + heading: number; + speed: number; + }; + timestamp: number; +}; + +export type LocationHistoryPoint = { + timestamp: number; +} & LonLatData; + +export type LonLatData = { + longitude: number; + latitude: number; +}; diff --git a/src/frontend/types/geojson-geometries-lookup.d.ts b/src/frontend/types/geojson-geometries-lookup.d.ts new file mode 100644 index 000000000..7d73efd6b --- /dev/null +++ b/src/frontend/types/geojson-geometries-lookup.d.ts @@ -0,0 +1,15 @@ +declare module 'geojson-geometries-lookup' { + import type { + GeoJSON, + Point, + LineString, + Polygon, + FeatureCollection, + } from '@types/geojson'; + + export default class GeojsonGeometriesLookup { + constructor(geoJson: GeoJSON); + + getContainers(geometry: Point | LineString | Polygon): FeatureCollection; + } +} diff --git a/src/frontend/types/osm_borders__maritime_10000m.d.ts b/src/frontend/types/osm_borders__maritime_10000m.d.ts new file mode 100644 index 000000000..b328ce97b --- /dev/null +++ b/src/frontend/types/osm_borders__maritime_10000m.d.ts @@ -0,0 +1,6 @@ +declare module '@osm_borders/maritime_10000m' { + import {GeoJSON} from 'geojson'; + + const data: GeoJSON; + export default data; +} diff --git a/src/frontend/utils/distance.ts b/src/frontend/utils/distance.ts new file mode 100644 index 000000000..2aefde679 --- /dev/null +++ b/src/frontend/utils/distance.ts @@ -0,0 +1,14 @@ +import CheapRuler from 'cheap-ruler'; +import {LonLatData} from '../sharedTypes/location'; + +export const calculateTotalDistance = (points: LonLatData[]): number => { + if (points.length <= 1) { + return 0; + } + + const ruler = new CheapRuler(points[0]!.latitude, 'kilometers'); + + return ruler.lineDistance( + points.map(point => [point.longitude, point.latitude]), + ); +};