mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
MOB-722 merge main
This commit is contained in:
@@ -20,7 +20,8 @@ module.exports = {
|
||||
"react-native",
|
||||
"simple-import-sort",
|
||||
"@tanstack/query",
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"@stylistic"
|
||||
],
|
||||
rules: {
|
||||
"arrow-parens": [2, "as-needed"],
|
||||
|
||||
208
.github/workflows/e2e_android.new.yml
vendored
Normal file
208
.github/workflows/e2e_android.new.yml
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
name: e2e-Android
|
||||
permissions:
|
||||
contents: read
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# 4-core Ubunutu GitHub Larger Runner
|
||||
# https://docs.github.com/en/enterprise-cloud@latest/billing/reference/actions-runner-pricing#x64-powered-larger-runners
|
||||
runs-on: ubuntu-24.04-m
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Generate the secret files needed for a release build
|
||||
- name: Create .env file
|
||||
env:
|
||||
OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }}
|
||||
OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}
|
||||
E2E_TEST_USERNAME: ${{ secrets.E2E_TEST_USERNAME }}
|
||||
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
|
||||
JWT_ANONYMOUS_API_SECRET: ${{ secrets.JWT_ANONYMOUS_API_SECRET }}
|
||||
GMAPS_API_KEY: ${{ secrets.GMAPS_API_KEY }}
|
||||
run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\nANDROID_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.tflite\nANDROID_TAXONOMY_FILE_NAME=taxonomy.csv\nANDROID_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.tflite\nIOS_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.mlmodel\nIOS_TAXONOMY_FILE_NAME=taxonomy.json\nIOS_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.mlmodel\nCV_MODEL_VERSION=small_2\n' "$JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" "$GMAPS_API_KEY" > .env
|
||||
|
||||
- name: Add secrets to google-services.json
|
||||
env:
|
||||
FIREBASE_PROJECT_NUMBER: ${{ secrets.FIREBASE_PROJECT_NUMBER }}
|
||||
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
|
||||
FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
|
||||
FIREBASE_STAGING_GOOGLE_APP_ID: ${{ secrets.FIREBASE_STAGING_GOOGLE_APP_ID }}
|
||||
FIREBASE_STAGING_PACKAGE_NAME: ${{ secrets.FIREBASE_STAGING_PACKAGE_NAME }}
|
||||
FIREBASE_STAGING_API_KEY: ${{ secrets.FIREBASE_STAGING_API_KEY }}
|
||||
run: |
|
||||
cp android/app/google-services.example.json android/app/google-services.json
|
||||
sed -i "s/your_project_number/${FIREBASE_PROJECT_NUMBER//\//\\/}/g" "android/app/google-services.json"
|
||||
sed -i "s/your_project_id/${FIREBASE_PROJECT_ID//\//\\/}/g" "android/app/google-services.json"
|
||||
sed -i "s/your_storage_bucket/${FIREBASE_STORAGE_BUCKET//\//\\/}/g" "android/app/google-services.json"
|
||||
sed -i "s/your_mobilesdk_app_id/${FIREBASE_STAGING_GOOGLE_APP_ID//\//\\/}/g" "android/app/google-services.json"
|
||||
sed -i "s/your_package_name/${FIREBASE_STAGING_PACKAGE_NAME//\//\\/}/g" "android/app/google-services.json"
|
||||
sed -i "s/your_current_key/${FIREBASE_STAGING_API_KEY//\//\\/}/g" "android/app/google-services.json"
|
||||
|
||||
- name: Create keystore.properties file
|
||||
env:
|
||||
ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
ANDROID_ALIAS: ${{ secrets.ANDROID_ALIAS }}
|
||||
run: printf 'storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=release.keystore' "$ANDROID_KEY_STORE_PASSWORD" "$ANDROID_KEY_PASSWORD" "$ANDROID_ALIAS" > android/keystore.properties
|
||||
|
||||
- name: Generate release keystore
|
||||
env:
|
||||
ANDROID_ALIAS: ${{ secrets.ANDROID_ALIAS }}
|
||||
ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
keytool -genkeypair -v -noprompt -storetype PKCS12 -keystore release.keystore -alias "$ANDROID_ALIAS" -keyalg RSA -keysize 2048 -validity 10000 -storepass "$ANDROID_KEY_STORE_PASSWORD" -keypass "$ANDROID_KEY_PASSWORD" -dname "CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB"
|
||||
|
||||
- name: Move keystore
|
||||
run: mv release.keystore android/app/release.keystore
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
gradle-version: '8.14.1'
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Download the small example cv and geomodel
|
||||
run: |
|
||||
npm run add-example-model -- -f=main
|
||||
|
||||
- name: Android Build
|
||||
run: |
|
||||
cd android
|
||||
# for the CI build, we can just target the x86 build for the emulator
|
||||
# also, we can exclude linting tasks
|
||||
./gradlew build -PreactNativeArchitectures=x86 -x lint -x lintVitalRelease
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: release-apk
|
||||
# note: clarifying because this is different from iOS:
|
||||
# {myBuild}.apk is a single file so `release-apk` points to a single file
|
||||
# in contrast, iOS's .ipa is actually a _directory_
|
||||
path: android/app/build/outputs/apk/release/*-release.apk
|
||||
|
||||
test:
|
||||
# 4-core Ubunutu GitHub Larger Runner
|
||||
# https://docs.github.com/en/enterprise-cloud@latest/billing/reference/actions-runner-pricing#x64-powered-larger-runners
|
||||
# `cores` setting for emulator actions should be updated to reflect changes to this
|
||||
runs-on: ubuntu-24.04-m
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
# note: this step requires checkout for the test flows, but does _not_ require npm install w/ Maestro
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: release-apk
|
||||
# resulting file structure: $GITHUB_HOME/e2e-build/{myBuild}.apk
|
||||
path: e2e-build
|
||||
|
||||
- name: Install Maestro
|
||||
run: |
|
||||
export MAESTRO_VERSION="2.0.8"; curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
|
||||
# These KVM settings are in support of the android-emulator-runner action that takes advantage of them
|
||||
# Further reading:
|
||||
# - https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
|
||||
# - https://github.com/reactivecircus/android-emulator-runner/tree/v2/?tab=readme-ov-file#github-action---android-emulator-runner
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
# AVD Cache: we can reuse a previously-created AVD to save time and make the test-run start step more dependable
|
||||
# both android-emulator-runner steps should have the same image config for cores, api-level, target
|
||||
- name: Attempt to restore AVD cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: avd-cache-restore
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-29
|
||||
- name: Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache-restore.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
cores: 4
|
||||
api-level: 29
|
||||
target: google_apis
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: false
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
- name: Save AVD cache
|
||||
if: steps.avd-cache-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-29
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
MAESTRO_DRIVER_STARTUP_TIMEOUT: 60000
|
||||
MAESTRO_CLI_NO_ANALYTICS: 1
|
||||
# custom ENV var to drive recordings within test flows
|
||||
# see README for details
|
||||
MAESTRO_RECORD: true
|
||||
with:
|
||||
cores: 4
|
||||
api-level: 29
|
||||
target: google_apis
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: |
|
||||
adb install -r e2e-build/*.apk
|
||||
# single exemplary "smoketest" flow for now
|
||||
maestro test --no-ansi e2e/maestro/android/signedOut.yaml
|
||||
|
||||
- name: Upload test video
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ success() || failure() }}
|
||||
with:
|
||||
# single exemplary flow artifact for now, generated by MAESTRO_RECORD configuration
|
||||
path: signedOut.mp4
|
||||
name: Android
|
||||
|
||||
notify:
|
||||
name: Notify Slack
|
||||
needs: test
|
||||
if: ${{ success() || failure() }}
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: iRoachie/slack-github-actions@v2.3.0
|
||||
if: env.SLACK_WEBHOOK_URL != null
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_BUILDS_WEBHOOK_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.github/workflows/e2e_android.yml
vendored
2
.github/workflows/e2e_android.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: e2e-Android
|
||||
name: e2e-Android-old
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -72,6 +72,10 @@ Note that you can run `npx jest` as well, but that will omit some environment va
|
||||
Also note that `i18next` needs to be initialized in individual test files (haven't figured out a way to await initialization before *all* tests, plus allowing tests to control initialization helps when testing different locales). Add `beforeAll( async ( ) => { await initI18next( ); } );` to a test file if it depends on localized text.
|
||||
|
||||
### E2E tests
|
||||
|
||||
> [!NOTE]
|
||||
> Status of e2e tests in CI: internal / staff PRs run iOS Detox tests as a PR check. Android Detox tests are currently disabled. We are currently working to restore Android e2e tests. For now, we have a skeleton Android flow running a single smoketest flow for simplicity. This is not a PR check yet and can only be run manually (`workflow_dispatch` GH Actions trigger).
|
||||
|
||||
We're using [Detox](https://wix.github.io/Detox/docs/introduction/getting-started/) for E2E tests. If you want to run the e2e tests on your local machine, make sure you follow the Detox environment setup instructions.
|
||||
|
||||
Then you have to populate `E2E_TEST_USERNAME` and `E2E_TEST_PASSWORD` in `.env` with real iNaturalist login credentials so the e2e test can actually authenticate.
|
||||
@@ -93,6 +97,13 @@ If you want to run the Android tests you need to prepare your environment. Befor
|
||||
|
||||
Run `npm run e2e:build:android && npm run e2e:test:android` to build the APK for testing purposes and install and run it on the emulator with the name as stated in the `.detoxrc.js` file.
|
||||
|
||||
#### Maestro
|
||||
|
||||
> [!NOTE]
|
||||
> By the Maestro's team own disclosure, the `maestro record` command for generating a test run recording is unusably slow. As a workaround, we use the inline `startRecording` Maestro directive within tests, which is more performant. For convenience, this is defined in an `onFlowStart.yaml` intented to be referred to in each flow's `onFlowStart` lifecycle hook.
|
||||
|
||||
Some desired test flows cannot be acheived with Detox and for those, we use Maestro. These are tests that rely on behavior external to the application such as deeplinks and sharing photos into the app. These are not currently run in CI as a PR check. The in-progress Android CI uses a Maestro smoketest flow as an exemplary command while we work on restoring Android Detox tests.
|
||||
|
||||
## Translations
|
||||
|
||||
### Adding and changing new source strings
|
||||
|
||||
@@ -107,7 +107,7 @@ android {
|
||||
applicationId "org.inaturalist.iNaturalistMobile"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 188
|
||||
versionCode 189
|
||||
versionName "1.0.12"
|
||||
setProperty("archivesBaseName", applicationId + "-v" + versionName + "+" + versionCode)
|
||||
manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
|
||||
|
||||
7
e2e/maestro/android/onFlowStart.yaml
Normal file
7
e2e/maestro/android/onFlowStart.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
appId: org.inaturalist.iNaturalistMobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${MAESTRO_RECORD == 'true'}
|
||||
commands:
|
||||
- startRecording: ${MAESTRO_FILENAME}
|
||||
22
e2e/maestro/android/signedOut.yaml
Normal file
22
e2e/maestro/android/signedOut.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
appId: org.inaturalist.iNaturalistMobile
|
||||
# FLGMwt: I would prefer to be able to configure setup/teardown
|
||||
# in workspace config, but this is not supported
|
||||
# https://github.com/mobile-dev-inc/Maestro/issues/2063
|
||||
onFlowStart:
|
||||
- runFlow: onFlowStart.yaml
|
||||
onFlowComplete:
|
||||
# noop if not recording
|
||||
- stopRecording
|
||||
---
|
||||
- launchApp:
|
||||
appId: org.inaturalist.iNaturalistMobile
|
||||
clearState: true
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "Close"
|
||||
timeout: 30000
|
||||
- waitForAnimationToEnd
|
||||
|
||||
- tapOn: Close
|
||||
|
||||
- assertVisible: Use iNaturalist to identify any living thing
|
||||
5
fastlane/metadata/android/en-US/changelogs/189.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/189.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
NEW
|
||||
* Increased analytics to figure out why "app is slow"
|
||||
|
||||
UPDATED
|
||||
* Saved observation screen in not-advanced mode
|
||||
@@ -548,7 +548,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNative.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -675,7 +675,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNativeRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
@@ -988,7 +988,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "iNaturalistReactNative-ShareExtension/iNaturalistReactNative-ShareExtension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
@@ -1033,7 +1033,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>188</string>
|
||||
<string>189</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
||||
306
package-lock.json
generated
306
package-lock.json
generated
@@ -122,6 +122,7 @@
|
||||
"@react-native/eslint-config": "0.80.2",
|
||||
"@react-native/metro-config": "0.80.2",
|
||||
"@react-native/typescript-config": "0.80.2",
|
||||
"@stylistic/eslint-plugin": "^3.1.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.28.11",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
@@ -6614,6 +6615,263 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz",
|
||||
"integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.13.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.40.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz",
|
||||
"integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.48.1",
|
||||
"@typescript-eslint/types": "^8.48.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz",
|
||||
"integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
"@typescript-eslint/visitor-keys": "8.48.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz",
|
||||
"integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz",
|
||||
"integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz",
|
||||
"integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.48.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.48.1",
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
"@typescript-eslint/visitor-keys": "8.48.1",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz",
|
||||
"integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.48.1",
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
"@typescript-eslint/typescript-estree": "8.48.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz",
|
||||
"integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||
@@ -22477,6 +22735,54 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"@react-native/eslint-config": "0.80.2",
|
||||
"@react-native/metro-config": "0.80.2",
|
||||
"@react-native/typescript-config": "0.80.2",
|
||||
"@stylistic/eslint-plugin": "^3.1.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.28.11",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
|
||||
@@ -91,6 +91,16 @@ const AddObsBottomSheet = ( {
|
||||
t
|
||||
] );
|
||||
|
||||
const optionRows = AI_CAMERA_SUPPORTED
|
||||
? [
|
||||
[obsCreateItems.standardCamera, obsCreateItems.photoLibrary],
|
||||
[obsCreateItems.soundRecorder, obsCreateItems.aiCamera]
|
||||
]
|
||||
: [
|
||||
[obsCreateItems.standardCamera],
|
||||
[obsCreateItems.soundRecorder, obsCreateItems.photoLibrary]
|
||||
];
|
||||
|
||||
const renderAddObsIcon = ( {
|
||||
accessibilityHint,
|
||||
accessibilityLabel,
|
||||
@@ -133,15 +143,11 @@ const AddObsBottomSheet = ( {
|
||||
>
|
||||
<View className="flex-column gap-y-4 pb-4 px-4">
|
||||
|
||||
<View className={ROW_CLASS}>
|
||||
{renderAddObsIcon( obsCreateItems.standardCamera )}
|
||||
{renderAddObsIcon( obsCreateItems.photoLibrary )}
|
||||
</View>
|
||||
|
||||
<View className={ROW_CLASS}>
|
||||
{renderAddObsIcon( obsCreateItems.soundRecorder )}
|
||||
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
|
||||
</View>
|
||||
{optionRows.map( row => (
|
||||
<View key={row.map( i => i.testID ).join( "-" )} className={ROW_CLASS}>
|
||||
{row.map( item => renderAddObsIcon( item ) )}
|
||||
</View>
|
||||
) )}
|
||||
|
||||
<Pressable
|
||||
className="bg-mediumGray w-full flex-row items-center py-[10px] px-5 rounded-lg
|
||||
|
||||
@@ -13,15 +13,29 @@ import {
|
||||
Modal,
|
||||
Portal
|
||||
} from "react-native-paper";
|
||||
import type { CameraDeviceFormat } from "react-native-vision-camera";
|
||||
import { useDebugMode } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import SliderControl from "./SliderControl";
|
||||
|
||||
interface Props {
|
||||
debugFormat: CameraDeviceFormat | null;
|
||||
changeDebugFormat: () => void;
|
||||
confidenceThreshold: number;
|
||||
setConfidenceThreshold: ( value: number ) => void;
|
||||
fps: number;
|
||||
setFPS: ( value: number ) => void;
|
||||
numStoredResults: number;
|
||||
setNumStoredResults: ( value: number ) => void;
|
||||
cropRatio: number;
|
||||
setCropRatio: ( value: number ) => void;
|
||||
}
|
||||
|
||||
const AIDebugButton = ( {
|
||||
debugFormat,
|
||||
changeDebugFormat,
|
||||
confidenceThreshold,
|
||||
debugFormat,
|
||||
setConfidenceThreshold,
|
||||
fps,
|
||||
setFPS,
|
||||
@@ -29,7 +43,7 @@ const AIDebugButton = ( {
|
||||
setNumStoredResults,
|
||||
cropRatio,
|
||||
setCropRatio
|
||||
} ) => {
|
||||
}: Props ) => {
|
||||
const [modalVisible, setModalVisible] = useState( false );
|
||||
const [slideIndex, setSlideIndex] = useState( 0 );
|
||||
const { isDebug } = useDebugMode( );
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
} from "providers/ExploreContext";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useCurrentUser } from "sharedHooks";
|
||||
import { useCurrentUser, useDebugMode } from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
import Explore from "./Explore";
|
||||
import ExploreV2 from "./ExploreV2";
|
||||
import mapParamsToAPI from "./helpers/mapParamsToAPI";
|
||||
import useExploreHeaderCount from "./hooks/useExploreHeaderCount";
|
||||
import useParams from "./hooks/useParams";
|
||||
@@ -24,6 +25,8 @@ import useParams from "./hooks/useParams";
|
||||
const ExploreContainerWithContext = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const { isDebug } = useDebugMode();
|
||||
|
||||
const exploreView = useStore( state => state.exploreView );
|
||||
const setExploreView = useStore( state => state.setExploreView );
|
||||
|
||||
@@ -133,32 +136,48 @@ const ExploreContainerWithContext = ( ): Node => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Explore
|
||||
canFetch={canFetch}
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
currentExploreView={exploreView}
|
||||
setCurrentExploreView={setExploreView}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
hideBackButton={false}
|
||||
filterByIconicTaxonUnknown={
|
||||
() => dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
|
||||
}
|
||||
isConnected={isConnected}
|
||||
isFetchingHeaderCount={isFetchingHeaderCount}
|
||||
openFiltersModal={openFiltersModal}
|
||||
queryParams={queryParams}
|
||||
showFiltersModal={showFiltersModal}
|
||||
updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
|
||||
updateLocation={updateLocation}
|
||||
updateUser={updateUser}
|
||||
updateProject={updateProject}
|
||||
placeMode={state.placeMode}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
/>
|
||||
{!isDebug
|
||||
? (
|
||||
<Explore
|
||||
canFetch={canFetch}
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
currentExploreView={exploreView}
|
||||
setCurrentExploreView={setExploreView}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
hideBackButton={false}
|
||||
filterByIconicTaxonUnknown={
|
||||
() => dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
|
||||
}
|
||||
isConnected={isConnected}
|
||||
isFetchingHeaderCount={isFetchingHeaderCount}
|
||||
openFiltersModal={openFiltersModal}
|
||||
queryParams={queryParams}
|
||||
showFiltersModal={showFiltersModal}
|
||||
updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
|
||||
updateLocation={updateLocation}
|
||||
updateUser={updateUser}
|
||||
updateProject={updateProject}
|
||||
placeMode={state.placeMode}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ExploreV2
|
||||
canFetch={canFetch}
|
||||
currentExploreView={exploreView}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
isConnected={isConnected}
|
||||
placeMode={state.placeMode}
|
||||
queryParams={queryParams}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
)}
|
||||
{renderPermissionsGate( {
|
||||
onPermissionGranted: startFetching
|
||||
} ) }
|
||||
|
||||
193
src/components/Explore/ExploreV2.tsx
Normal file
193
src/components/Explore/ExploreV2.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { refresh } from "@react-native-community/netinfo";
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
Body2,
|
||||
Button,
|
||||
INatIconButton,
|
||||
OfflineNotice,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { PLACE_MODE } from "providers/ExploreContext";
|
||||
import React from "react";
|
||||
import { Alert } from "react-native";
|
||||
import {
|
||||
useDebugMode,
|
||||
useStoredLayout,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
import type { RenderLocationPermissionsGateFunction } from "sharedHooks/useLocationPermission";
|
||||
import { getShadow } from "styles/global";
|
||||
|
||||
import IdentifiersView from "./IdentifiersView";
|
||||
import ObservationsView from "./ObservationsView";
|
||||
import ObservationsViewBar from "./ObservationsViewBar";
|
||||
import ObserversView from "./ObserversView";
|
||||
import SpeciesView from "./SpeciesView";
|
||||
|
||||
const DROP_SHADOW = getShadow( {
|
||||
offsetHeight: 4,
|
||||
elevation: 6
|
||||
} );
|
||||
|
||||
enum EXPLORE_VIEW {
|
||||
OBSERVATIONS = "observations",
|
||||
IDENTIFIERS = "identifiers",
|
||||
OBSERVERS = "observers",
|
||||
SPECIES = "species"
|
||||
}
|
||||
|
||||
enum EXPLORE_OBSERVATIONS_LAYOUT {
|
||||
GRID = "grid",
|
||||
LIST = "list",
|
||||
MAP = "map"
|
||||
}
|
||||
|
||||
interface Props {
|
||||
canFetch?: boolean;
|
||||
currentExploreView: EXPLORE_VIEW;
|
||||
handleUpdateCount: ( exploreView: EXPLORE_VIEW, totalResults: number ) => void;
|
||||
hasLocationPermissions?: boolean;
|
||||
isConnected: boolean;
|
||||
placeMode: string;
|
||||
queryParams: object;
|
||||
renderLocationPermissionsGate: RenderLocationPermissionsGateFunction;
|
||||
requestLocationPermissions: () => void;
|
||||
}
|
||||
|
||||
const ExploreV2 = ( {
|
||||
canFetch,
|
||||
currentExploreView,
|
||||
handleUpdateCount,
|
||||
hasLocationPermissions,
|
||||
isConnected,
|
||||
placeMode,
|
||||
queryParams,
|
||||
renderLocationPermissionsGate,
|
||||
requestLocationPermissions
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const { layout, writeLayoutToStorage } = useStoredLayout( "exploreObservationsLayout" ) as {
|
||||
layout: EXPLORE_OBSERVATIONS_LAYOUT | null;
|
||||
writeLayoutToStorage: ( newValue: EXPLORE_OBSERVATIONS_LAYOUT ) => void;
|
||||
};
|
||||
const { isDebug } = useDebugMode( );
|
||||
|
||||
const renderMainContent = ( ) => {
|
||||
if ( isConnected === false ) {
|
||||
return (
|
||||
<OfflineNotice
|
||||
onPress={() => refresh()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// hasLocationPermissions === undefined means we haven't checked for location permissions yet
|
||||
if ( placeMode === PLACE_MODE.NEARBY && hasLocationPermissions === false ) {
|
||||
return (
|
||||
<View className="flex-1 justify-center p-4">
|
||||
<View className="items-center">
|
||||
<Body2>{t( "To-view-nearby-organisms-please-enable-location" )}</Body2>
|
||||
</View>
|
||||
<Button
|
||||
className="mt-5"
|
||||
text={t( "ALLOW-LOCATION-ACCESS" )}
|
||||
accessibilityHint={t( "Opens-location-permission-prompt" )}
|
||||
level="focus"
|
||||
onPress={( ) => requestLocationPermissions()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{currentExploreView === EXPLORE_VIEW.OBSERVATIONS && (
|
||||
<ObservationsView
|
||||
canFetch={canFetch}
|
||||
layout={layout}
|
||||
queryParams={queryParams}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
renderLocationPermissionsGate={renderLocationPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === EXPLORE_VIEW.SPECIES && (
|
||||
<SpeciesView
|
||||
canFetch={canFetch}
|
||||
isConnected={isConnected}
|
||||
queryParams={queryParams}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === EXPLORE_VIEW.OBSERVERS && (
|
||||
<ObserversView
|
||||
canFetch={canFetch}
|
||||
isConnected={isConnected}
|
||||
queryParams={queryParams}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === EXPLORE_VIEW.IDENTIFIERS && (
|
||||
<IdentifiersView
|
||||
canFetch={canFetch}
|
||||
isConnected={isConnected}
|
||||
queryParams={queryParams}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper testID="ExploreV2" wrapperClassName="overflow-hidden">
|
||||
<View className="flex-1 overflow-hidden">
|
||||
{currentExploreView === "observations" && (
|
||||
<ObservationsViewBar
|
||||
layout={layout}
|
||||
updateObservationsView={writeLayoutToStorage}
|
||||
/>
|
||||
)}
|
||||
{renderMainContent()}
|
||||
{isDebug && (
|
||||
<INatIconButton
|
||||
icon="triangle-exclamation"
|
||||
className={classnames(
|
||||
"absolute",
|
||||
"bg-white",
|
||||
"bottom-[100px]",
|
||||
"h-[55px]",
|
||||
"right-5",
|
||||
"rounded-full",
|
||||
"w-[55px]",
|
||||
"z-10"
|
||||
)}
|
||||
color="white"
|
||||
size={27}
|
||||
style={[
|
||||
DROP_SHADOW,
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
{ backgroundColor: "deeppink" }
|
||||
]}
|
||||
accessibilityLabel="Diagnostics"
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
"ExploreV2 Info",
|
||||
`queryParams: ${JSON.stringify( queryParams )}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ViewWrapper>
|
||||
{/*
|
||||
Leaving this here so that it is easier to reason about differences between Explore
|
||||
and ExploreRedesign.
|
||||
*/}
|
||||
{null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreV2;
|
||||
@@ -25,7 +25,7 @@ import MapView from "./MapView";
|
||||
|
||||
type Props = {
|
||||
canFetch?: boolean,
|
||||
layout: string,
|
||||
layout: ?string,
|
||||
queryParams: Object,
|
||||
handleUpdateCount: Function,
|
||||
hasLocationPermissions?: boolean,
|
||||
|
||||
@@ -9,7 +9,7 @@ import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
hideMap?: boolean,
|
||||
layout: string,
|
||||
layout: ?string,
|
||||
updateObservationsView: Function
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ import React, {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useCurrentUser } from "sharedHooks";
|
||||
import { useCurrentUser, useDebugMode } from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
import Explore from "./Explore";
|
||||
import ExploreV2 from "./ExploreV2";
|
||||
import mapParamsToAPI from "./helpers/mapParamsToAPI";
|
||||
import useExploreHeaderCount from "./hooks/useExploreHeaderCount";
|
||||
|
||||
@@ -29,6 +30,7 @@ const RootExploreContainerWithContext = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const { isDebug } = useDebugMode();
|
||||
const rootExploreView = useStore( state => state.rootExploreView );
|
||||
const setRootExploreView = useStore( state => state.setRootExploreView );
|
||||
const rootStoredParams = useStore( state => state.rootStoredParams );
|
||||
@@ -211,32 +213,48 @@ const RootExploreContainerWithContext = ( ): Node => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Explore
|
||||
canFetch={canFetch}
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
hideBackButton
|
||||
filterByIconicTaxonUnknown={
|
||||
() => dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
|
||||
}
|
||||
currentExploreView={rootExploreView}
|
||||
setCurrentExploreView={setRootExploreView}
|
||||
isConnected={isConnected}
|
||||
isFetchingHeaderCount={isFetchingHeaderCount}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
openFiltersModal={openFiltersModal}
|
||||
queryParams={queryParams}
|
||||
showFiltersModal={showFiltersModal}
|
||||
updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
|
||||
updateLocation={updateLocation}
|
||||
updateUser={updateUser}
|
||||
updateProject={updateProject}
|
||||
placeMode={state.placeMode}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
/>
|
||||
{!isDebug
|
||||
? (
|
||||
<Explore
|
||||
canFetch={canFetch}
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
hideBackButton
|
||||
filterByIconicTaxonUnknown={
|
||||
() => dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
|
||||
}
|
||||
currentExploreView={rootExploreView}
|
||||
setCurrentExploreView={setRootExploreView}
|
||||
isConnected={isConnected}
|
||||
isFetchingHeaderCount={isFetchingHeaderCount}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
openFiltersModal={openFiltersModal}
|
||||
queryParams={queryParams}
|
||||
showFiltersModal={showFiltersModal}
|
||||
updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
|
||||
updateLocation={updateLocation}
|
||||
updateUser={updateUser}
|
||||
updateProject={updateProject}
|
||||
placeMode={state.placeMode}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ExploreV2
|
||||
canFetch={canFetch}
|
||||
currentExploreView={rootExploreView}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
isConnected={isConnected}
|
||||
placeMode={state.placeMode}
|
||||
queryParams={queryParams}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
)}
|
||||
{renderPermissionsGate( {
|
||||
onPermissionGranted: async ( ) => {
|
||||
await updateLocation( "nearby" );
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from "components/styledComponents";
|
||||
import _, { compact } from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Image as RNImage } from "react-native";
|
||||
import Photo from "realmModels/Photo";
|
||||
import type { RealmObservationPhoto, RealmPhoto, RealmTaxon } from "realmModels/types";
|
||||
import getImageDimensions from "sharedHelpers/getImageDimensions";
|
||||
|
||||
type Props = {
|
||||
representativePhoto?: ApiPhoto,
|
||||
@@ -73,7 +73,7 @@ const PhotosSection = ( {
|
||||
useEffect( ( ) => {
|
||||
const checkImageOrientation = async ( ) => {
|
||||
if ( observationPhoto ) {
|
||||
const imageDimensions = await getImageDimensions( observationPhoto );
|
||||
const imageDimensions = await RNImage.getSize( observationPhoto );
|
||||
if ( imageDimensions.width < imageDimensions.height ) {
|
||||
setDisplayPortraitLayout( true );
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ScrollView, View } from "components/styledComponents";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Image } from "react-native";
|
||||
import Photo from "realmModels/Photo";
|
||||
import getImageDimensions from "sharedHelpers/getImageDimensions";
|
||||
|
||||
import PhotoContainer from "./PhotoContainer";
|
||||
import SoundContainer from "./SoundContainer";
|
||||
@@ -25,7 +25,7 @@ const MasonryLayout = ( { items, onImagePress } ) => {
|
||||
if ( item.file_url ) {
|
||||
return item;
|
||||
}
|
||||
const imageDimensions = await getImageDimensions( photoUrl( item ) );
|
||||
const imageDimensions = await Image.getSize( photoUrl( item ) );
|
||||
return Object.assign( item, imageDimensions );
|
||||
} );
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Image } from "react-native";
|
||||
|
||||
type ImageDimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
// A helper function to get the image dimensions
|
||||
const getImageDimensions = async ( uri: string ) => new Promise<ImageDimensions>( resolve => {
|
||||
Image.getSize( uri, ( width, height ) => {
|
||||
resolve( { width, height } );
|
||||
} );
|
||||
} );
|
||||
|
||||
export default getImageDimensions;
|
||||
@@ -18,6 +18,10 @@ export interface LocationPermissionCallbacks {
|
||||
onModalHide?: ( ) => void;
|
||||
}
|
||||
|
||||
export type RenderLocationPermissionsGateFunction = (
|
||||
callbacks: LocationPermissionCallbacks | undefined
|
||||
) => React.JSX.Element | null;
|
||||
|
||||
/**
|
||||
* A hook to check and request location permissions.
|
||||
* @returns {boolean} hasPermissions - Undefined if permissions have not been checked yet.
|
||||
@@ -33,45 +37,49 @@ const useLocationPermission = ( ) => {
|
||||
|
||||
// PermissionGate callbacks need to use useCallback, otherwise they'll
|
||||
// trigger re-renders if/when they change
|
||||
const renderPermissionsGate = useCallback( ( callbacks?: LocationPermissionCallbacks ) => {
|
||||
const {
|
||||
onPermissionGranted,
|
||||
onPermissionDenied,
|
||||
onPermissionBlocked,
|
||||
onModalHide
|
||||
} = callbacks || { };
|
||||
const renderPermissionsGate: RenderLocationPermissionsGateFunction
|
||||
= useCallback(
|
||||
( callbacks?: LocationPermissionCallbacks ) => {
|
||||
const {
|
||||
onPermissionGranted,
|
||||
onPermissionDenied,
|
||||
onPermissionBlocked,
|
||||
onModalHide
|
||||
} = callbacks || {};
|
||||
|
||||
// this prevents infinite rerenders of the LocationPermissionGate component
|
||||
if ( !showPermissionGate ) {
|
||||
return null;
|
||||
}
|
||||
// this prevents infinite rerenders of the LocationPermissionGate component
|
||||
if ( !showPermissionGate ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LocationPermissionGate
|
||||
permissionNeeded
|
||||
withoutNavigation
|
||||
onModalHide={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
if ( onModalHide ) onModalHide( );
|
||||
}}
|
||||
onPermissionGranted={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
setHasPermissions( true );
|
||||
setHasBlockedPermissions( false );
|
||||
if ( onPermissionGranted ) onPermissionGranted( );
|
||||
}}
|
||||
onPermissionDenied={( ) => {
|
||||
if ( onPermissionDenied ) onPermissionDenied( );
|
||||
}}
|
||||
onPermissionBlocked={( ) => {
|
||||
setHasPermissions( false );
|
||||
setHasBlockedPermissions( true );
|
||||
setShowPermissionGate( true );
|
||||
if ( onPermissionBlocked ) onPermissionBlocked( );
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<LocationPermissionGate
|
||||
permissionNeeded
|
||||
withoutNavigation
|
||||
onModalHide={() => {
|
||||
setShowPermissionGate( false );
|
||||
if ( onModalHide ) onModalHide();
|
||||
}}
|
||||
onPermissionGranted={() => {
|
||||
setShowPermissionGate( false );
|
||||
setHasPermissions( true );
|
||||
setHasBlockedPermissions( false );
|
||||
if ( onPermissionGranted ) onPermissionGranted();
|
||||
}}
|
||||
onPermissionDenied={() => {
|
||||
if ( onPermissionDenied ) onPermissionDenied();
|
||||
}}
|
||||
onPermissionBlocked={() => {
|
||||
setHasPermissions( false );
|
||||
setHasBlockedPermissions( true );
|
||||
setShowPermissionGate( true );
|
||||
if ( onPermissionBlocked ) onPermissionBlocked();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[showPermissionGate]
|
||||
);
|
||||
}, [showPermissionGate] );
|
||||
|
||||
// This gets exported and used as a dependency, so it needs to have
|
||||
// referential stability
|
||||
|
||||
@@ -6,9 +6,7 @@ import { Image } from "react-native";
|
||||
import factory from "tests/factory";
|
||||
import faker from "tests/helpers/faker";
|
||||
|
||||
Image.getSize = jest.fn( ( uri, callback ) => {
|
||||
callback( { width: 1024, height: 768 } );
|
||||
} );
|
||||
Image.getSize = jest.fn( ( _uri, _callback ) => async () => ( { width: 1024, height: 768 } ) );
|
||||
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
created_at: "2022-11-27T19:07:41-08:00",
|
||||
|
||||
Reference in New Issue
Block a user