diff --git a/Gemfile b/Gemfile
index a18c55dd25..2104c88096 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
ruby '>= 2.6.10'
gem 'cocoapods', '>= 1.11.3'
+gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
diff --git a/Gemfile.lock b/Gemfile.lock
index 898336ab29..4b82b239e5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,15 +3,10 @@ GEM
specs:
CFPropertyList (3.0.6)
rexml
- activesupport (7.1.2)
- base64
- bigdecimal
+ activesupport (7.0.8)
concurrent-ruby (~> 1.0, >= 1.0.2)
- connection_pool (>= 2.2.5)
- drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
- mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
@@ -21,24 +16,22 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.869.0)
- aws-sdk-core (3.190.0)
+ aws-partitions (1.881.0)
+ aws-sdk-core (3.190.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.75.0)
+ aws-sdk-kms (1.76.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.141.0)
+ aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
- base64 (0.2.0)
- bigdecimal (3.1.5)
claide (1.1.0)
cocoapods (1.14.3)
addressable (~> 2.8)
@@ -82,19 +75,16 @@ GEM
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.2.2)
- connection_pool (2.4.1)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
- domain_name (0.6.20231109)
+ domain_name (0.6.20240107)
dotenv (2.8.1)
- drb (2.2.0)
- ruby2_keywords
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
- excon (0.108.0)
+ excon (0.109.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -123,8 +113,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
- fastimage (2.2.7)
- fastlane (2.217.0)
+ fastimage (2.3.0)
+ fastlane (2.218.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -151,7 +141,7 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
- optparse (~> 0.1.1)
+ optparse (>= 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
@@ -184,7 +174,7 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-apis-storage_v1 (0.29.0)
+ google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
@@ -192,11 +182,11 @@ GEM
google-cloud-env (2.1.0)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1)
- google-cloud-storage (1.45.0)
+ google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
- google-apis-storage_v1 (~> 0.29.0)
+ google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
@@ -218,18 +208,17 @@ GEM
jwt (2.7.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
- minitest (5.20.0)
+ minitest (5.21.1)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.3.0)
- mutex_m (0.2.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
- optparse (0.1.1)
+ optparse (0.4.0)
os (1.1.4)
- plist (3.7.0)
+ plist (3.7.1)
public_suffix (4.0.7)
rake (13.1.0)
representable (3.2.0)
@@ -283,6 +272,7 @@ PLATFORMS
ruby
DEPENDENCIES
+ activesupport (>= 6.1.7.3, < 7.1.0)
cocoapods (>= 1.11.3)
fastlane
fastlane-plugin-versioning_android
diff --git a/MIT-LICENSE b/MIT-LICENSE
index ea541d1cae..708eb1636c 100644
--- a/MIT-LICENSE
+++ b/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2023 iNaturalist
+Copyright (c) 2024 iNaturalist
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 43775338c1..9f3802b1c6 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -114,8 +114,8 @@ android {
applicationId "org.inaturalist.seek"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 326
- versionName "2.15.5"
+ versionCode 332
+ versionName "2.15.6"
// for creating ic_seek_adaptiveappicon.xml
vectorDrawables.useSupportLibrary = true
}
diff --git a/components/Auth/PrivacyPolicyScreen.js b/components/Auth/PrivacyPolicyScreen.js
index 6cfd9561e4..22d953375e 100644
--- a/components/Auth/PrivacyPolicyScreen.js
+++ b/components/Auth/PrivacyPolicyScreen.js
@@ -388,7 +388,7 @@ const PrivacyPolicyScreen = (): React.Node => {
- © Copyright 2023 iNaturalist. All Rights Reserved.
+ © Copyright 2024 iNaturalist. All Rights Reserved.
`;
const seekHtml = `Last Revised on July 11, 2023
@@ -484,7 +484,7 @@ const PrivacyPolicyScreen = (): React.Node => {
San Rafael, CA 94915-0357
-© Copyright 2023 iNaturalist. All Rights Reserved.
+© Copyright 2024 iNaturalist. All Rights Reserved.
Revised on July 11, 2023.
`;
diff --git a/components/Auth/TermsOfServiceScreen.js b/components/Auth/TermsOfServiceScreen.js
index a544b2e8ee..f2058e7a01 100644
--- a/components/Auth/TermsOfServiceScreen.js
+++ b/components/Auth/TermsOfServiceScreen.js
@@ -178,7 +178,7 @@ const TermsOfServiceScreen = ( ): React.Node => {
- © Copyright 2023 iNaturalist. All rights reserved.
+ © Copyright 2024 iNaturalist. All rights reserved.
`;
const DEFAULT_PROPS = {
diff --git a/components/Home/Announcements/Announcements.js b/components/Home/Announcements/Announcements.js
index de7035010b..9dc9050672 100644
--- a/components/Home/Announcements/Announcements.js
+++ b/components/Home/Announcements/Announcements.js
@@ -65,7 +65,9 @@ const Announcements = ( ): React.Node => {
};
const accessToken = await fetchAccessToken();
const apiToken = await fetchJSONWebToken( accessToken );
- const options = { api_token: apiToken, user_agent: createUserAgent() };
+ const headers = {};
+ headers["user-agent"] = createUserAgent();
+ const options = { api_token: apiToken, headers };
inatjs.announcements
.search( params, options )
.then( ( { total_results, results } ) => {
diff --git a/components/Modals/WarningModal.js b/components/Modals/WarningModal.js
index a2eef15622..1d25d40330 100644
--- a/components/Modals/WarningModal.js
+++ b/components/Modals/WarningModal.js
@@ -31,6 +31,13 @@ const WarningModal = ( { closeModal }: Props ): React.Node => (
+
+ {i18n.t( "warning.tip_0" )}
+
+
{[1, 2, 3].map( ( warning ) => {
const iconName = icons[`warning_${warning}`];
@@ -47,7 +54,10 @@ const WarningModal = ( { closeModal }: Props ): React.Node => (
);
} )}
-
+
{i18n.t( "warning.tip_4" )}
diff --git a/components/PostToiNat/PostScreen.js b/components/PostToiNat/PostScreen.js
index a1317c091e..a5dc802f83 100644
--- a/components/PostToiNat/PostScreen.js
+++ b/components/PostToiNat/PostScreen.js
@@ -68,7 +68,7 @@ const PostScreen = ( ): Node => {
longitude: preciseLong,
observed_on_string: initialDate,
place_guess: null,
- positional_accuracy: accuracy,
+ positional_accuracy: Math.trunc( accuracy ),
taxon_id: taxaId,
// this shows that the id is recommended by computer vision
vision: true
@@ -91,7 +91,7 @@ const PostScreen = ( ): Node => {
...editedObservation,
latitude: coords.latitude,
longitude: coords.longitude,
- positional_accuracy: coords.accuracy
+ positional_accuracy: Math.trunc( coords.accuracy )
} } );
}, [editedObservation] );
diff --git a/components/PostToiNat/hooks/postingHooks.js b/components/PostToiNat/hooks/postingHooks.js
index 2989901440..72fdd78cd6 100644
--- a/components/PostToiNat/hooks/postingHooks.js
+++ b/components/PostToiNat/hooks/postingHooks.js
@@ -20,7 +20,9 @@ const useSearchSpecies = ( speciesName: ?string ): any => {
locale: i18n.locale
};
- const options = { user_agent: createUserAgent( ) };
+ const headers = {};
+ headers["user-agent"] = createUserAgent();
+ const options = { headers };
inatjs.taxa.autocomplete( params, options ).then( ( { results } ) => {
if ( results.length === 0 ) { return; }
diff --git a/components/Providers/ObservationProvider.js b/components/Providers/ObservationProvider.js
index 1995d2ca6f..d3757839c3 100644
--- a/components/Providers/ObservationProvider.js
+++ b/components/Providers/ObservationProvider.js
@@ -1,6 +1,6 @@
// @flow
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useEffect, useCallback, useRef } from "react";
import type { Node } from "react";
import { Platform } from "react-native";
import inatjs from "inaturalistjs";
@@ -57,7 +57,9 @@ const ObservationProvider = ( { children }: Props ): Node => {
};
const fetchPhoto = useCallback( async ( id ) => {
- const options = { user_agent: createUserAgent( ) };
+ const headers = {};
+ headers["user-agent"] = createUserAgent();
+ const options = { headers };
// probably should break this into a helper function to use in other places
// like species nearby fetches for better offline experience
@@ -86,6 +88,7 @@ const ObservationProvider = ( { children }: Props ): Node => {
}
}, [] );
+ const currentSpeciesID = useRef( null );
const handleSpecies = useCallback( async ( param ) => {
if ( !observation ) { return; }
const { predictions, errorCode, latitude } = observation.image;
@@ -115,6 +118,16 @@ const ObservationProvider = ( { children }: Props ): Node => {
};
};
+ // Only run this once for a given species because fetchSpeciesSeenDate throws an error in
+ // a C++ library of realm if the function is called twice. Even though this error only happens
+ // for the first time a user observes a species that counts towards a challenge, having this check
+ // here does not have any negative effects on the app I think.
+ if ( currentSpeciesID.current === species.taxon_id ) {
+ currentSpeciesID.current = null;
+ return;
+ } else {
+ currentSpeciesID.current = species.taxon_id;
+ }
const seenDate = await fetchSpeciesSeenDate( Number( species.taxon_id ) );
const mediumPhoto = await fetchPhoto( species.taxon_id );
@@ -272,7 +285,9 @@ const ObservationProvider = ( { children }: Props ): Node => {
const fetchOnlineVisionResults = async ( ) => {
const uploadParams = await flattenUploadParameters( image );
const token = createJwtToken( );
- const options = { api_token: token, user_agent: createUserAgent() };
+ const headers = {};
+ headers["user-agent"] = createUserAgent();
+ const options = { api_token: token, headers };
try {
const r = await inatjs.computervision.score_image( uploadParams, options );
diff --git a/components/SeekYearInReview/SeekYearInReviewMap.js b/components/SeekYearInReview/SeekYearInReviewMap.js
index 4481fc4e3d..55aa9b46a4 100644
--- a/components/SeekYearInReview/SeekYearInReviewMap.js
+++ b/components/SeekYearInReview/SeekYearInReviewMap.js
@@ -14,9 +14,8 @@ import GreenButton from "../UIComponents/Buttons/GreenButton";
import { getBounds, getCenterOfBounds } from "geolib";
type Props = {
- +region: Object,
- +id: number,
- +seenDate: ?string,
+ +year: number,
+ +observations: Array