diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml
index a7f8dc971..eea994f2c 100644
--- a/.github/workflows/e2e_android.yml
+++ b/.github/workflows/e2e_android.yml
@@ -289,7 +289,7 @@ jobs:
name: Notify Slack
needs: test
if: ${{ success() || failure() }}
- runs-on: macos-latest
+ runs-on: ubuntu-latest
steps:
- uses: iRoachie/slack-github-actions@v2.3.0
if: env.SLACK_WEBHOOK_URL != null
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b2a9931f..d9fedcca5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,7 +24,7 @@ GEM
ast (2.4.3)
atomos (0.1.3)
aws-eventstream (1.4.0)
- aws-partitions (1.1263.0)
+ aws-partitions (1.1267.0)
aws-sdk-core (3.252.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -102,7 +102,8 @@ GEM
ethon (0.18.0)
ffi (>= 1.15.0)
logger
- excon (0.112.0)
+ excon (1.5.0)
+ logger
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -132,10 +133,10 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
- fastlane (2.236.1)
+ fastlane (2.237.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
- addressable (>= 2.8, < 3.0.0)
+ addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
@@ -147,7 +148,7 @@ GEM
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
- excon (>= 0.71.0, < 1.0.0)
+ excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
@@ -212,7 +213,7 @@ GEM
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.6.0)
- google-cloud-storage (1.61.0)
+ google-cloud-storage (1.62.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
diff --git a/README.md b/README.md
index 0a6d20e8c..5366830c9 100644
--- a/README.md
+++ b/README.md
@@ -348,7 +348,7 @@ fastlane prod
1. Change to observations view and view an observation
1. Go to the observers profile
1. Go to one project this user joined (if not available try another user)
- 1. Sign in
+ 1. Log in
1. Make an observation by taking a new photo in the AI Camera; tap "Upload Now" to upload immediately; wait for upload to complete before moving on
1. Make an observation by taking a new photo in the StandardCamera; save without upload
1. Make an observation by importing an existing; save without upload
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1ce82033e..0099597fc 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,7 +107,7 @@ android {
applicationId "org.inaturalist.iNaturalistMobile"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 219
+ versionCode 220
versionName "1.0.25"
manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
// Detox Android setup
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 7e3b83c6e..632c039a2 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -510,11 +510,11 @@ lane :beta do
build_number = get_build_number( xcodeproj: XCODEPROJ )
- # UI.message "Play Store: promoting build #{build_number} to beta track"
+ UI.message "Play Store: promoting build #{build_number} to Closed beta track"
upload_to_play_store(
version_code: build_number,
track: "internal",
- track_promote_to: "beta"
+ track_promote_to: "Closed Testing 2026"
)
UI.message "TestFlight: adding build #{build_number} to Open Beta group"
diff --git a/fastlane/metadata/android/en-US/changelogs/220.txt b/fastlane/metadata/android/en-US/changelogs/220.txt
new file mode 100644
index 000000000..111bf8706
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/220.txt
@@ -0,0 +1,4 @@
+FIXED
+* Adding comments to Agreeing IDs
+* A crash when deleting photo
+* Location chooser spinning indefinitely
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 6477a98e6..ab0725227 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -3960,7 +3960,7 @@ PODS:
- VisionCamera/React (4.7.3):
- React-Core
- VisionCamera/FrameProcessors
- - VisionCameraPluginInatVision (6.0.0-rc.3):
+ - VisionCameraPluginInatVision (6.0.0):
- boost
- DoubleConversion
- fast_float
@@ -4564,7 +4564,7 @@ SPEC CHECKSUMS:
SensitiveInfo: 09107b865dc23b4e9c0a61ec01385387bf3391a8
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
VisionCamera: 0044a94f7489f19e19d5938e97dfc36f4784af3c
- VisionCameraPluginInatVision: 561ffc82eb61315312414a9f4424c5d6411c83ab
+ VisionCameraPluginInatVision: 3b006a7a434ddfc23d789729b6d98e451f90c4e4
Yoga: e9fea23bf9b2766818ce9a8df72f584bf5ad36cd
PODFILE CHECKSUM: 73e24a8590e365c9fec451abf69476ae07116d98
diff --git a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj
index 2dda9258b..8aab700d2 100644
--- a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj
+++ b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj
@@ -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 = 219;
+ CURRENT_PROJECT_VERSION = 220;
DEVELOPMENT_TEAM = N5J7L4P93Z;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
@@ -674,7 +674,7 @@
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNativeRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
- CURRENT_PROJECT_VERSION = 219;
+ CURRENT_PROJECT_VERSION = 220;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
HEADER_SEARCH_PATHS = (
@@ -990,7 +990,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "iNaturalistReactNative-ShareExtension/iNaturalistReactNative-ShareExtension.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 219;
+ CURRENT_PROJECT_VERSION = 220;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
@@ -1035,7 +1035,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 219;
+ CURRENT_PROJECT_VERSION = 220;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
diff --git a/ios/iNaturalistReactNative/Info.plist b/ios/iNaturalistReactNative/Info.plist
index 8b47d3fa1..432e7aca8 100644
--- a/ios/iNaturalistReactNative/Info.plist
+++ b/ios/iNaturalistReactNative/Info.plist
@@ -42,7 +42,7 @@
CFBundleVersion
- 219
+ 220
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/package-lock.json b/package-lock.json
index b669dc74b..1a64c1c7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -116,7 +116,7 @@
"realm": "^20.2.0",
"sanitize-html": "^2.17.4",
"uuid": "^11.1.1",
- "vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#e8c4782cee82e781e8dc7016fffedbbd95070931",
+ "vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#0354705c124e3a9fabcca3e5881e64cd93d2f0df",
"zustand": "^4.5.7"
},
"devDependencies": {
@@ -23084,9 +23084,9 @@
}
},
"node_modules/vision-camera-plugin-inatvision": {
- "version": "6.0.0-rc.3",
- "resolved": "git+ssh://git@github.com/inaturalist/vision-camera-plugin-inatvision.git#e8c4782cee82e781e8dc7016fffedbbd95070931",
- "integrity": "sha512-aarLhPkpgxtFSjrbcbNtzL2VZNM8So+hBzVQROFIEQrj5e/dPp8zWGQ4gONRW9rkUH7t1pmWD4125Eq1/w45+g==",
+ "version": "6.0.0",
+ "resolved": "git+ssh://git@github.com/inaturalist/vision-camera-plugin-inatvision.git#0354705c124e3a9fabcca3e5881e64cd93d2f0df",
+ "integrity": "sha512-HYfaSSb5WnjOLLWLioF7ZgXlk8He8YmoPNV789mhDUBFBgXPaE452O+qW7mIcYVV6cz/WDt+qW3C1ImLEDAWQw==",
"license": "MIT",
"workspaces": [
"example"
diff --git a/package.json b/package.json
index a1b4810c8..9d671d47b 100644
--- a/package.json
+++ b/package.json
@@ -153,7 +153,7 @@
"realm": "^20.2.0",
"sanitize-html": "^2.17.4",
"uuid": "^11.1.1",
- "vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#e8c4782cee82e781e8dc7016fffedbbd95070931",
+ "vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#0354705c124e3a9fabcca3e5881e64cd93d2f0df",
"zustand": "^4.5.7"
},
"devDependencies": {
diff --git a/patches/react-native-maps+1.27.1.patch b/patches/react-native-maps+1.27.1.patch
new file mode 100644
index 000000000..47605c0c9
--- /dev/null
+++ b/patches/react-native-maps+1.27.1.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-maps/ios/AirMaps/RNMapsMapView.mm b/node_modules/react-native-maps/ios/AirMaps/RNMapsMapView.mm
+index 447520d..8043dcb 100644
+--- a/node_modules/react-native-maps/ios/AirMaps/RNMapsMapView.mm
++++ b/node_modules/react-native-maps/ios/AirMaps/RNMapsMapView.mm
+@@ -60,7 +60,7 @@ - (void)animateToRegion:(NSString *)regionJSON duration:(NSInteger)duration{
+ NSDictionary* regionDic = [RCTConvert dictonaryFromString:regionJSON];
+ MKCoordinateRegion region = [RCTConvert MKCoordinateRegion:regionDic];
+ Boolean animated = duration == 0 ? NO :YES;
+- _view.ignoreRegionChanges = animated;
++ // _view.ignoreRegionChanges = animated;
+ [_view setRegion:region animated:animated];
+ }
+ - (void)setCamera:(NSString *)cameraJSON{
diff --git a/src/api/fields.ts b/src/api/fields.ts
index 238b3f347..3515c4e14 100644
--- a/src/api/fields.ts
+++ b/src/api/fields.ts
@@ -30,6 +30,7 @@ export const PROJECT_DETAIL_FIELDS = {
header_image_url: true,
membership_model: true,
place_id: true,
+ project_observation_fields: PROJECT_OBSERVATION_FIELDS_FIELDS,
user_ids: true,
};
diff --git a/src/api/types.d.ts b/src/api/types.d.ts
index 9f8e54922..c0fa06866 100644
--- a/src/api/types.d.ts
+++ b/src/api/types.d.ts
@@ -63,6 +63,7 @@ export interface ApiProject extends ApiProjectSummary {
header_image_url: string | null;
membership_model: "inviteonly" | "open" | null;
place_id: number | null;
+ project_observation_fields: ApiProjectObservationField[];
user_ids: number[];
}
diff --git a/src/components/Explore/ExploreV2/components/DefaultSearchOptions.tsx b/src/components/Explore/ExploreV2/components/DefaultSearchOptions.tsx
new file mode 100644
index 000000000..1a9d13912
--- /dev/null
+++ b/src/components/Explore/ExploreV2/components/DefaultSearchOptions.tsx
@@ -0,0 +1,97 @@
+import type { ApiTaxon } from "api/types";
+import UniversalSearchResult
+ from "components/Explore/ExploreV2/components/UniversalSearchResult";
+import { resultToSubject }
+ from "components/Explore/ExploreV2/helpers/universalSearchSubject";
+import type { UniversalSearchResultItem }
+ from "components/Explore/ExploreV2/hooks/useUniversalSearch";
+import IconicTaxonChooser from "components/SharedComponents/IconicTaxonChooser";
+import Body1 from "components/SharedComponents/Typography/Body1";
+import { Pressable, ScrollView, View } from "components/styledComponents";
+import type { ExploreV2Subject } from "providers/ExploreV2Context";
+import React, { useCallback } from "react";
+import Taxon from "realmModels/Taxon";
+import type { RealmTaxon } from "realmModels/types";
+import useCurrentUser from "sharedHooks/useCurrentUser";
+import useIconicTaxa from "sharedHooks/useIconicTaxa";
+import useTranslation from "sharedHooks/useTranslation";
+
+interface Props {
+ onSelectSubject: ( subject: ExploreV2Subject ) => void;
+}
+
+const ROW_CLASSES = "px-[15px] py-[11px] border-b border-lightGray";
+
+const ICONIC_ROW_STYLE = {
+ alignItems: "center",
+ paddingHorizontal: 15,
+ paddingVertical: 15,
+} as const;
+
+const EMPTY_CHOSEN: string[] = [];
+
+const DefaultSearchOptions = ( { onSelectSubject }: Props ) => {
+ const { t } = useTranslation( );
+
+ const currentUser = useCurrentUser( );
+ const iconicTaxa = useIconicTaxa( );
+
+ const handleIconicTaxon = useCallback( ( iconicTaxonName: string ) => {
+ // Resolve the chooser's name-based callback into a full taxon subject
+ const taxaList = ( iconicTaxa
+ ? Array.from( iconicTaxa )
+ : [] ) as unknown as RealmTaxon[];
+ const realmTaxon = taxaList.find(
+ taxon => taxon?.name?.toLowerCase( ) === iconicTaxonName,
+ );
+ if ( !realmTaxon ) { return; }
+ const taxon = Taxon.mapRealmToPojo( realmTaxon ) as ApiTaxon;
+ onSelectSubject( resultToSubject( { type: "taxon", taxon } ) );
+ }, [iconicTaxa, onSelectSubject] );
+
+ const currentUserResult: UniversalSearchResultItem | null = currentUser?.id
+ ? {
+ type: "user",
+ user: {
+ id: currentUser.id,
+ login: currentUser.login,
+ icon_url: currentUser.icon_url,
+ observations_count: currentUser.observations_count,
+ },
+ }
+ : null;
+
+ return (
+
+
+
+
+ {currentUserResult && (
+ onSelectSubject( resultToSubject( currentUserResult ) )}
+ />
+ )}
+ {currentUser?.id && (
+ undefined}
+ testID="DefaultSearchOptions.unobserved"
+ >
+ {t( "Species-I-havent-observed" )}
+
+ )}
+
+ );
+};
+
+export default React.memo( DefaultSearchOptions );
diff --git a/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx b/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
index e16ff0aa5..4ba27c032 100644
--- a/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
+++ b/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
@@ -1,5 +1,7 @@
import { useNavigation } from "@react-navigation/native";
import classnames from "classnames";
+import DefaultSearchOptions
+ from "components/Explore/ExploreV2/components/DefaultSearchOptions";
import LocationSearchResult
from "components/Explore/ExploreV2/components/LocationSearchResult";
import UniversalSearchResult
@@ -188,6 +190,23 @@ const UniversalSearch = ( ) => {
? locationData
: subjectData;
+ const showDefaultOptions = !showLocation && !subjectHasQuery;
+ const listEmptyComponent = showDefaultOptions
+ ? ( )
+ : (
+
+ );
+
return (
@@ -261,19 +280,7 @@ const UniversalSearch = ( ) => {
keyboardShouldPersistTaps="handled"
keyExtractor={resultKey}
renderItem={renderItem}
- ListEmptyComponent={(
-
- )}
+ ListEmptyComponent={listEmptyComponent}
/>
diff --git a/src/components/LocationPicker/LocationPicker.js b/src/components/LocationPicker/LocationPicker.tsx
similarity index 77%
rename from src/components/LocationPicker/LocationPicker.js
rename to src/components/LocationPicker/LocationPicker.tsx
index dd69141cf..875f599dc 100644
--- a/src/components/LocationPicker/LocationPicker.js
+++ b/src/components/LocationPicker/LocationPicker.tsx
@@ -1,5 +1,3 @@
-// @flow
-
import classnames from "classnames";
import {
CloseButton,
@@ -9,8 +7,9 @@ import {
} from "components/SharedComponents";
import { SharedStackViewWrapper } from "components/SharedComponents/ViewWrapper";
import { View } from "components/styledComponents";
-import type { Node } from "react";
import React from "react";
+import type { LayoutChangeEvent } from "react-native";
+import type { Region } from "react-native-maps";
import { useTranslation } from "sharedHooks";
import CrosshairCircle from "./CrosshairCircle";
@@ -18,23 +17,24 @@ import DisplayLatLng from "./DisplayLatLng";
import Footer from "./Footer";
import LoadingIndicator from "./LoadingIndicator";
import LocationSearch from "./LocationSearch";
+import type { LocationPickerPlace } from "./types";
-type Props = {
- accuracy: number,
- handleSave: Function,
- hidePlaceResults: boolean,
- loading: boolean,
- locationName: string,
- initialRegion: Object,
- onCurrentLocationPress: Function,
- onMapReady: Function,
- onRegionChangeComplete: Function,
- region: Object,
- regionToAnimate: Object,
- selectPlaceResult: Function,
- updateLocationName: Function,
- onMapLayout: Function,
-};
+interface Props {
+ accuracy: number;
+ handleSave: ( ) => void;
+ hidePlaceResults: boolean;
+ loading: boolean;
+ locationName: string;
+ initialRegion: Region | null;
+ onCurrentLocationPress: ( ) => void;
+ onMapReady: ( ) => void;
+ onRegionChangeComplete: ( newRegion: Region ) => void;
+ region: Region;
+ regionToAnimate: Region;
+ selectPlaceResult: ( place: LocationPickerPlace ) => void;
+ updateLocationName: ( name: string ) => void;
+ onMapLayout: ( event: LayoutChangeEvent ) => void;
+}
const LocationPicker = ( {
accuracy,
@@ -51,7 +51,7 @@ const LocationPicker = ( {
selectPlaceResult,
updateLocationName,
onMapLayout,
-}: Props ): Node => {
+}: Props ) => {
const { t } = useTranslation( );
let regionToDisplay;
@@ -110,6 +110,9 @@ const LocationPicker = ( {