Merge branch 'main' into MOB-974-safe-area-view-context-cutover

This commit is contained in:
Ryan Stelly
2025-11-04 07:10:51 -06:00
21 changed files with 2004 additions and 1147 deletions

View File

@@ -84,6 +84,7 @@ module.exports = {
"no-var": 1,
"operator-linebreak": [2, "before"],
"prefer-const": [2, { destructuring: "all" }],
"no-restricted-syntax": 0,
// "react/forbid-prop-types": 0,
"react/prop-types": 0,
"react/destructuring-assignment": 0,

View File

@@ -117,6 +117,20 @@ node_modules/react-native/Libraries/polyfills/.*
.*/node_modules/react-native/src/private/webapis/dom/nodes/internals/NodeInternals.js
.*/node_modules/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentElementInstanceHandle.js
.*/node_modules/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentInstanceHandle.js
.*/node_modules/@react-native/community-cli-plugin/dist/commands/bundle/assetPathUtils.js.flow
.*/node_modules/@react-native/dev-middleware/dist/inspector-proxy/Device.js.flow
.*/node_modules/react-native/Libraries/Animated/components/AnimatedFlatList.js
.*/node_modules/react-native/Libraries/Animated/components/AnimatedSectionList.js
.*/node_modules/react-native/Libraries/Components/ToastAndroid/ToastAndroid.js.flow
.*/node_modules/react-native/Libraries/StyleSheet/StyleSheet.js
.*/node_modules/react-native/Libraries/StyleSheet/StyleSheet.js.flow
.*/node_modules/react-native/Libraries/StyleSheet/StyleSheetExports.js.flow
.*/node_modules/react-native/Libraries/StyleSheet/flattenStyle.js
.*/node_modules/react-native/Libraries/StyleSheet/private/_TransformStyle.js
.*/node_modules/react-native/Libraries/Utilities/PlatformTypes.js
.*/node_modules/react-native/Libraries/vendor/core/ErrorUtils.js
.*/node_modules/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js
.*/node_modules/react-native/src/private/devsupport/rndevtools/ReactDevToolsSettingsManager.js.flow
[untyped]
.*/node_modules/@react-native-community/cli/.*/.*

View File

@@ -4,13 +4,11 @@ import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
@@ -35,10 +33,6 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
loadReactNative(this)
}
}

View File

@@ -9,7 +9,7 @@ buildscript {
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
kotlinVersion = "2.1.20"
// This specifies which LiteRT version to use for our vision-plugin.
litertVersion = "1.4.0"
}

View File

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

4
android/gradlew vendored
View File

@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

9
android/gradlew.bat vendored
View File

@@ -1,3 +1,8 @@
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
@REM
@REM This source code is licensed under the MIT license found in the
@REM LICENSE file in the root directory of this source tree.
@rem
@rem Copyright 2015 the original author or authors.
@rem
@@ -70,11 +75,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -4,7 +4,6 @@ import {
import { iNatE2eAfterEach, iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import closeOnboarding from "./sharedFlows/closeOnboarding";
import deleteObservation from "./sharedFlows/deleteObservation";
import signIn from "./sharedFlows/signIn";
import uploadObservation from "./sharedFlows/uploadObservation";
@@ -22,7 +21,7 @@ describe( "AICamera", () => {
/*
/ 1. Sign in
*/
const username = await signIn();
await signIn();
/*
/ 2. Take photo with AI Camera, select a suggestion, upload and delete observation
@@ -68,13 +67,6 @@ describe( "AICamera", () => {
0
);
await waitFor( displayTaxonName ).toBeVisible().withTimeout( TIMEOUT );
await displayTaxonName.tap();
// Delete the observation
await deleteObservation( { uploaded: true } );
// Make sure we're back on MyObservations
await waitFor( username ).toBeVisible().withTimeout( TIMEOUT );
}
);
} );

View File

@@ -124,10 +124,14 @@ export default async function resetUserForTesting() {
console.log( `Deleting ${observationIdsToDelete.length} observations` );
await Promise.all( observationIdsToDelete.map( async uuid => {
await inatjs.observations.delete(
{ uuid },
opts
);
try {
await inatjs.observations.delete(
{ uuid },
opts
);
} catch ( _error ) {
console.log( `Could not delete observation: ${uuid}. Moving on...` );
}
} ) );
console.log( "Creating sample observation" );

View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,19 @@
<string>org.inaturalist.iNaturalistMobile</string>
<key>HostAppURLScheme</key>
<string>inaturalistmobile://</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
@@ -21,31 +34,20 @@
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
<key>RCTNewArchEnabled</key>
<true/>
<key>ReactShareViewBackgroundColor</key>
<dict>
<key>Red</key>
<integer>1</integer>
<key>Green</key>
<integer>1</integer>
<key>Blue</key>
<integer>1</integer>
<key>Alpha</key>
<integer>1</integer>
<key>Transparent</key>
<false/>
</dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
<key>Alpha</key>
<integer>1</integer>
<key>Blue</key>
<integer>1</integer>
<key>Green</key>
<integer>1</integer>
<key>Red</key>
<integer>1</integer>
<key>Transparent</key>
<false/>
</dict>
</dict>
</plist>

View File

@@ -73,6 +73,8 @@
<string>Export iNaturalist photos to your library.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Export and import iNaturalist photos to and from your library.</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>UIAppFonts</key>
<array>
<string>Lato-Bold.ttf</string>

801
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -88,10 +88,10 @@
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"nativewind": "^2.0.11",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^14.1.0",
"react-native": "0.79.5",
"react-native": "0.80.2",
"react-native-animated-dots-carousel": "^2.0.0",
"react-native-audio-recorder-player": "^3.6.7",
"react-native-bouncy-checkbox": "^3.0.7",
@@ -150,13 +150,13 @@
"@babel/runtime": "^7.25.0",
"@faker-js/faker": "^8.4.1",
"@fluent/syntax": "^0.19.0",
"@react-native-community/cli": "18.0.0",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.5",
"@react-native/eslint-config": "0.79.5",
"@react-native/metro-config": "0.79.5",
"@react-native/typescript-config": "0.79.5",
"@react-native-community/cli": "19.1.1",
"@react-native-community/cli-platform-android": "19.1.1",
"@react-native-community/cli-platform-ios": "19.1.1",
"@react-native/babel-preset": "0.80.2",
"@react-native/eslint-config": "0.80.2",
"@react-native/metro-config": "0.80.2",
"@react-native/typescript-config": "0.80.2",
"@tanstack/eslint-plugin-query": "^5.28.11",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3",
@@ -164,9 +164,9 @@
"@types/jsrsasign": "^10.5.15",
"@types/lodash": "^4.17.20",
"@types/markdown-it": "^14.1.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react-test-renderer": "^19.0.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@types/react-test-renderer": "^19.1.0",
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"babel-plugin-module-resolver": "^5.0.0",
@@ -203,7 +203,7 @@
"patch-package": "^8.0.1",
"react-native-clean-project": "^4.0.3",
"react-native-config-node": "^0.0.3",
"react-test-renderer": "19.0.0",
"react-test-renderer": "19.1.0",
"reassure": "^1.1.0",
"tailwindcss": "^3.3.2",
"ts-node": "^10.9.2",

View File

@@ -851,9 +851,9 @@ This-observation-needs-more-identifications = Bu gözlemin Araştırma Sınıfı
This-observation-needs-more-identifications-to-become-research-grade = Bu gözlemin araştırma sınıfı statüsü kazanması için daha çok tanımlamaya ihtiyacı var.
This-observer-has-opted-out-of-the-Community-Taxon = Bu gözlemci Topluluk Taksonu'nundan vazgeçti
This-organism-was-placed-by-humans = Bu organizma bu konuma insanlar tarafından yerleştirilmiştir. Bu durum bahçe bitkileri, evcil hayvanlar ve hayvanat bahçesi hayvanları gibi şeyler için geçerlidir.
This-user-has-no-followers = This user has no followers.
This-user-has-not-joined-any-projects = This user has not joined any projects.
This-user-is-not-following-anyone = This user is not following anyone.
This-user-has-no-followers = Bu kullanıcının takipçisi bulunmamaktadır.
This-user-has-not-joined-any-projects = Bu kullanıcı henüz hiçbir projeye katılmadı.
This-user-is-not-following-anyone = Bu kullanıcı hiç kimseyi takip etmiyor.
To-sync-your-observations-to-iNaturalist = Gözlemlerinizi iNaturalist ile eşitlemek için lütfen giriş yapın.
To-view-nearby-organisms-please-enable-location = Yakındaki organizmaları görüntülemek için lütfen konumu etkinleştir.
To-view-nearby-projects-please-enable-location = Yakındaki projeleri görüntülemek için lütfen konumu etkinleştir.

View File

@@ -797,9 +797,9 @@
"This-observation-needs-more-identifications-to-become-research-grade": "Bu gözlemin araştırma sınıfı statüsü kazanması için daha çok tanımlamaya ihtiyacı var.",
"This-observer-has-opted-out-of-the-Community-Taxon": "Bu gözlemci Topluluk Taksonu'nundan vazgeçti",
"This-organism-was-placed-by-humans": "Bu organizma bu konuma insanlar tarafından yerleştirilmiştir. Bu durum bahçe bitkileri, evcil hayvanlar ve hayvanat bahçesi hayvanları gibi şeyler için geçerlidir.",
"This-user-has-no-followers": "This user has no followers.",
"This-user-has-not-joined-any-projects": "This user has not joined any projects.",
"This-user-is-not-following-anyone": "This user is not following anyone.",
"This-user-has-no-followers": "Bu kullanıcının takipçisi bulunmamaktadır.",
"This-user-has-not-joined-any-projects": "Bu kullanıcı henüz hiçbir projeye katılmadı.",
"This-user-is-not-following-anyone": "Bu kullanıcı hiç kimseyi takip etmiyor.",
"To-sync-your-observations-to-iNaturalist": "Gözlemlerinizi iNaturalist ile eşitlemek için lütfen giriş yapın.",
"To-view-nearby-organisms-please-enable-location": "Yakındaki organizmaları görüntülemek için lütfen konumu etkinleştir.",
"To-view-nearby-projects-please-enable-location": "Yakındaki projeleri görüntülemek için lütfen konumu etkinleştir.",

View File

@@ -147,6 +147,11 @@ class Taxon extends Realm.Object {
return taxonForUpdate;
}
/**
* @param {object} taxon
* @param {Realm} _realm
* @returns {object}
*/
static mapApiToRealm( taxon, _realm = null ) {
return {
...taxon,

View File

@@ -1,20 +1,31 @@
import { fetchTaxon } from "api/taxa";
import { getJWT } from "components/LoginSignUp/AuthenticationService";
import Realm, { UpdateMode } from "realm";
import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
async function fetchTaxonAndSave( id, realm, params = {}, opts = {} ) {
interface Options {
api_token?: string | null;
}
async function fetchTaxonAndSave(
id: number,
realm: Realm,
params: Record<string, unknown> = {},
opts: Options = {}
) {
const options = { ...opts };
if ( !options.api_token ) {
options.api_token = await getJWT( );
}
const remoteTaxon = await fetchTaxon( id, params, options );
// Casting is necessary until fetchTaxon is typed to return Taxon.
const remoteTaxon = await fetchTaxon( id, params, options ) as Taxon;
const mappedRemoteTaxon = Taxon.mapApiToRealm( remoteTaxon, realm );
safeRealmWrite( realm, ( ) => {
realm.create(
"Taxon",
Taxon,
Taxon.forUpdate( mappedRemoteTaxon ),
"modified"
UpdateMode.Modified
);
}, "saving remote taxon in ObsDetails" );
return mappedRemoteTaxon;

View File

@@ -1,12 +1,18 @@
// adapted from native Android iNaturalist app
// https://github.com/inaturalist/iNaturalistAndroid/blob/main/iNaturalist/src/main/java/org/inaturalist/android/UTFGrid.java
interface UTFGrid {
grid: string[];
keys: string[];
data?: { [key: string]: unknown };
}
const EXPANSION_PIXELS = 16;
const TILE_SIZE = 256;
const EMPTY_KEY = "";
const decodeId = id => {
const decodeId = ( id: number ): number => {
let decodedId = id;
if ( id >= 93 ) decodedId -= 1;
if ( id >= 35 ) decodedId -= 1;
@@ -14,7 +20,7 @@ const decodeId = id => {
return decodedId;
};
const getKeyForPixel = ( row, col, json ) => {
const getKeyForPixel = ( row: number, col: number, json: UTFGrid ): string => {
let id = 0;
if ( ( row >= 0 ) && ( col >= 0 )
@@ -32,7 +38,11 @@ const getKeyForPixel = ( row, col, json ) => {
return key;
};
const getKeyForPixelExpansive = ( x, y, json ) => {
const getKeyForPixelExpansive = (
x: number,
y: number,
json: UTFGrid
): string | null => {
if ( !json?.grid ) return null;
const factor = TILE_SIZE / json.grid.length;
@@ -73,13 +83,17 @@ const getKeyForPixelExpansive = ( x, y, json ) => {
/** Returns the data object corresponding to the given tile position
* @return data object corresponding to the tile position (null if no data for that position)
*/
const getDataForPixel = ( x, y, json ) => {
const getDataForPixel = (
x: number,
y: number,
json: UTFGrid | null | undefined
): unknown | null => {
if ( !json || !json?.data || !json?.grid ) return null;
const key = getKeyForPixelExpansive( x, y, json );
// Non existent key
if ( !json.data[key] ) { return null; }
if ( key === null || !json.data[key] ) { return null; }
return json.data[key];
};

View File

@@ -1,5 +1,5 @@
{
"extends": "@react-native/typescript-config/tsconfig.json",
"extends": "@react-native/typescript-config",
"compilerOptions": {
"baseUrl": ".",
"paths": {