From 02385038ef9ebeab693c37eadeaaf13af271f2f9 Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Wed, 24 Jun 2026 12:24:51 -0700 Subject: [PATCH 01/14] update variable name --- src/components/AddObsBottomSheet/AddObsButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AddObsBottomSheet/AddObsButton.js b/src/components/AddObsBottomSheet/AddObsButton.js index 0b8dae60c..c64c9eaa0 100644 --- a/src/components/AddObsBottomSheet/AddObsButton.js +++ b/src/components/AddObsBottomSheet/AddObsButton.js @@ -46,10 +46,10 @@ const AddObsButton = ( ): React.Node => { // Base trigger condition in all cases: // Only show the tooltip if the user has the AI camera as the default button option. // Only show the tooltip on MyObservations screen. - const onObsList = currentRoute?.name === "MyObservationsResults"; + const isOnMyObservationsResults = currentRoute?.name === "MyObservationsResults"; const onlyAiCamera = !isAllAddObsOptionsMode; - let triggerCondition = onObsList && onlyAiCamera; + let triggerCondition = isOnMyObservationsResults && onlyAiCamera; if ( justFinishedSignup ) { // If a user creates a new account, they should see the tooltip right after dismissing the From c9c1e6ff26d9a2477193a9cd58b06c8b04fc5914 Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Wed, 24 Jun 2026 14:09:21 -0700 Subject: [PATCH 02/14] add searched taxon banner and active search state --- .../MyObservations/MyObservationsSimple.tsx | 7 +++ .../MyObservationsSimpleHeader.tsx | 6 +++ .../Search/SearchMyObservationsTaxon.tsx | 7 +++ .../Search/SearchedTaxonBanner.tsx | 53 +++++++++++++++++++ src/providers/MyObservationsContext.tsx | 1 + 5 files changed, 74 insertions(+) create mode 100644 src/components/MyObservations/Search/SearchedTaxonBanner.tsx diff --git a/src/components/MyObservations/MyObservationsSimple.tsx b/src/components/MyObservations/MyObservationsSimple.tsx index 3599cda53..ffa0361e1 100644 --- a/src/components/MyObservations/MyObservationsSimple.tsx +++ b/src/components/MyObservations/MyObservationsSimple.tsx @@ -35,6 +35,8 @@ import { } from "sharedHelpers/speciesSort"; import { accessibleTaxonName } from "sharedHelpers/taxon"; import { useGridLayout, useLayoutPrefs, useTranslation } from "sharedHooks"; +import useFeatureFlag from "sharedHooks/useFeatureFlag"; +import { FeatureFlag } from "stores/createFeatureFlagSlice"; import colors from "styles/tailwindColors"; import type { SpeciesCount } from "types/sorting"; @@ -42,6 +44,7 @@ import LoginSheet from "./LoginSheet"; import { ACTIVE_SHEET } from "./MyObservationsResults"; import MyObservationsSimpleHeader from "./MyObservationsSimpleHeader"; import PivotCardObsGridItem from "./PivotCardObsGridItem"; +import SearchedTaxonBanner from "./Search/SearchedTaxonBanner"; import SimpleHeader from "./SimpleHeader"; import SimpleTaxonGridItem from "./SimpleTaxonGridItem"; @@ -129,6 +132,9 @@ const MyObservationsSimple = ( { }: Props ) => { const { isDefaultMode } = useLayoutPrefs( ); const { t } = useTranslation( ); + const searchMyObservationsEnabled = useFeatureFlag( + FeatureFlag.SearchMyObservationsEnabled, + ); const speciesSortLabels = useSpeciesSortLabels( ); const navigation = useNavigation( ); const route = useRoute( ); @@ -365,6 +371,7 @@ const MyObservationsSimple = ( { ]} TabComponent={renderTabComponent} /> + {searchMyObservationsEnabled && } { activeTab === OBSERVATIONS_TAB && ( <> { if ( !isConnected ) { @@ -111,6 +114,9 @@ const MyObservationsSimpleHeader = ( { diff --git a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx index 1a700d1c0..bcc069b53 100644 --- a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx +++ b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx @@ -14,6 +14,7 @@ import { useMyObservations, } from "providers/MyObservationsContext"; import React, { useCallback, useState } from "react"; +import type { RealmTaxon } from "realmModels/types"; import { useTranslation } from "sharedHooks"; import useTaxonSearch from "sharedHooks/useTaxonSearch"; @@ -29,12 +30,18 @@ const SearchMyObservationsTaxon = ( ) => { const onTaxonSelected = useCallback( ( newTaxon: ApiTaxon | null ) => { if ( newTaxon && typeof newTaxon.id === "number" && newTaxon.name ) { + // useTaxonSearch can return either ApiTaxon-shaped or RealmTaxon-shaped + // taxa depending on the source, so we have to check for both here. + // TODO: normalize taxa at ingest. + const iconUri = newTaxon.default_photo?.url + || ( newTaxon as unknown as RealmTaxon ).defaultPhoto?.url; dispatch( { type: MY_OBSERVATIONS_ACTION.SET_TAXON_SEARCH, searchTaxon: { id: newTaxon.id, name: newTaxon.name, preferred_common_name: newTaxon.preferred_common_name, + iconUri, }, } ); } else { diff --git a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx new file mode 100644 index 000000000..076243411 --- /dev/null +++ b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx @@ -0,0 +1,53 @@ +import { + Body3, + INatIconButton, +} from "components/SharedComponents"; +import { Image, View } from "components/styledComponents"; +import { + MY_OBSERVATIONS_ACTION, + useMyObservations, +} from "providers/MyObservationsContext"; +import React from "react"; +import { useTranslation } from "sharedHooks"; + +const SearchedTaxonBanner = ( ) => { + const { t } = useTranslation( ); + const { state, dispatch } = useMyObservations( ); + const { searchedTaxon } = state; + + if ( !searchedTaxon ) return null; + + const displayName = searchedTaxon.preferred_common_name || searchedTaxon.name; + + return ( + + + + {searchedTaxon.iconUri && ( + + )} + + + {displayName} + + + dispatch( { + type: MY_OBSERVATIONS_ACTION.CLEAR_TAXON_SEARCH, + } )} + /> + + ); +}; + +export default SearchedTaxonBanner; diff --git a/src/providers/MyObservationsContext.tsx b/src/providers/MyObservationsContext.tsx index 8ccad9dce..da1935b85 100644 --- a/src/providers/MyObservationsContext.tsx +++ b/src/providers/MyObservationsContext.tsx @@ -13,6 +13,7 @@ export interface MyObservationsTaxon { id: number; name: string; preferred_common_name?: string; + iconUri?: string; } export interface MyObservationsState { From 72182e2a7bdd636f886f716092cf9751c8fcea94 Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Wed, 24 Jun 2026 14:17:51 -0700 Subject: [PATCH 03/14] update type to normalize to non-api naming conventions --- .../MyObservations/Search/SearchMyObservationsTaxon.tsx | 7 +++++-- .../MyObservations/Search/SearchedTaxonBanner.tsx | 2 +- src/providers/MyObservationsContext.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx index bcc069b53..d18d9e06a 100644 --- a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx +++ b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx @@ -33,14 +33,17 @@ const SearchMyObservationsTaxon = ( ) => { // useTaxonSearch can return either ApiTaxon-shaped or RealmTaxon-shaped // taxa depending on the source, so we have to check for both here. // TODO: normalize taxa at ingest. + const realmTaxon = newTaxon as unknown as RealmTaxon; const iconUri = newTaxon.default_photo?.url - || ( newTaxon as unknown as RealmTaxon ).defaultPhoto?.url; + || realmTaxon.defaultPhoto?.url; + const preferredCommonName = newTaxon.preferred_common_name + || realmTaxon.preferredCommonName; dispatch( { type: MY_OBSERVATIONS_ACTION.SET_TAXON_SEARCH, searchTaxon: { id: newTaxon.id, name: newTaxon.name, - preferred_common_name: newTaxon.preferred_common_name, + preferredCommonName, iconUri, }, } ); diff --git a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx index 076243411..926e56bbf 100644 --- a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx +++ b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx @@ -17,7 +17,7 @@ const SearchedTaxonBanner = ( ) => { if ( !searchedTaxon ) return null; - const displayName = searchedTaxon.preferred_common_name || searchedTaxon.name; + const displayName = searchedTaxon.preferredCommonName || searchedTaxon.name; return ( Date: Wed, 24 Jun 2026 14:51:40 -0700 Subject: [PATCH 04/14] searched taxon banner should respect name preferences --- .../Search/SearchedTaxonBanner.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx index 926e56bbf..a409d2a1e 100644 --- a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx +++ b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx @@ -8,16 +8,29 @@ import { useMyObservations, } from "providers/MyObservationsContext"; import React from "react"; -import { useTranslation } from "sharedHooks"; +import type { RealmUser } from "realmModels/types"; +import { useCurrentUser, useTranslation } from "sharedHooks"; const SearchedTaxonBanner = ( ) => { const { t } = useTranslation( ); const { state, dispatch } = useMyObservations( ); const { searchedTaxon } = state; + // The Realm `User` class doesn't declare its schema fields as typed + // instance properties (see realmModels/User.ts), so cast to the typed + // RealmUser interface to access the user's name-display prefs. + const currentUser = useCurrentUser( ) as RealmUser | null; + if ( !searchedTaxon ) return null; - const displayName = searchedTaxon.preferredCommonName || searchedTaxon.name; + // Mirrors DisplayTaxonName: show scientific name when the user has + // opted into scientific-first or opted out of common names; + // otherwise show common name (falling back to scientific if missing). + const preferScientific = currentUser?.prefers_scientific_name_first === true + || currentUser?.prefers_common_names === false; + const displayName = preferScientific + ? searchedTaxon.name + : ( searchedTaxon.preferredCommonName || searchedTaxon.name ); return ( Date: Wed, 24 Jun 2026 14:52:24 -0700 Subject: [PATCH 05/14] remove double type cast --- .../MyObservations/Search/SearchMyObservationsTaxon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx index d18d9e06a..0d413538c 100644 --- a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx +++ b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx @@ -33,7 +33,7 @@ const SearchMyObservationsTaxon = ( ) => { // useTaxonSearch can return either ApiTaxon-shaped or RealmTaxon-shaped // taxa depending on the source, so we have to check for both here. // TODO: normalize taxa at ingest. - const realmTaxon = newTaxon as unknown as RealmTaxon; + const realmTaxon = newTaxon as RealmTaxon; const iconUri = newTaxon.default_photo?.url || realmTaxon.defaultPhoto?.url; const preferredCommonName = newTaxon.preferred_common_name From 9d137beb55587db7180506d3f5747318ce1003b1 Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Wed, 24 Jun 2026 17:09:55 -0700 Subject: [PATCH 06/14] display currently searched taxon on subsequent search screen visits, add helper for user name prefs --- .../Search/SearchMyObservationsTaxon.tsx | 16 ++++++++++++---- .../Search/SearchedTaxonBanner.tsx | 10 ++-------- src/sharedHelpers/taxon.ts | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx index 0d413538c..f4b58786e 100644 --- a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx +++ b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx @@ -14,15 +14,23 @@ import { useMyObservations, } from "providers/MyObservationsContext"; import React, { useCallback, useState } from "react"; -import type { RealmTaxon } from "realmModels/types"; -import { useTranslation } from "sharedHooks"; +import type { RealmTaxon, RealmUser } from "realmModels/types"; +import { taxonDisplayName } from "sharedHelpers/taxon"; +import { useCurrentUser, useTranslation } from "sharedHooks"; import useTaxonSearch from "sharedHooks/useTaxonSearch"; const SearchMyObservationsTaxon = ( ) => { const { t } = useTranslation( ); const navigation = useNavigation( ); - const { dispatch } = useMyObservations( ); - const [taxonQuery, setTaxonQuery] = useState( "" ); + const { state, dispatch } = useMyObservations( ); + const { searchedTaxon } = state; + const currentUser = useCurrentUser( ) as RealmUser | null; + + const [taxonQuery, setTaxonQuery] = useState( ( ) => ( + searchedTaxon + ? taxonDisplayName( searchedTaxon, currentUser ) + : "" + ) ); const { taxa, isLoading, isLocal } = useTaxonSearch( taxonQuery ); diff --git a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx index a409d2a1e..39097405b 100644 --- a/src/components/MyObservations/Search/SearchedTaxonBanner.tsx +++ b/src/components/MyObservations/Search/SearchedTaxonBanner.tsx @@ -9,6 +9,7 @@ import { } from "providers/MyObservationsContext"; import React from "react"; import type { RealmUser } from "realmModels/types"; +import { taxonDisplayName } from "sharedHelpers/taxon"; import { useCurrentUser, useTranslation } from "sharedHooks"; const SearchedTaxonBanner = ( ) => { @@ -23,14 +24,7 @@ const SearchedTaxonBanner = ( ) => { if ( !searchedTaxon ) return null; - // Mirrors DisplayTaxonName: show scientific name when the user has - // opted into scientific-first or opted out of common names; - // otherwise show common name (falling back to scientific if missing). - const preferScientific = currentUser?.prefers_scientific_name_first === true - || currentUser?.prefers_common_names === false; - const displayName = preferScientific - ? searchedTaxon.name - : ( searchedTaxon.preferredCommonName || searchedTaxon.name ); + const displayName = taxonDisplayName( searchedTaxon, currentUser ); return ( instead. +export function taxonDisplayName( + taxon: TaxonName, + user: User | null, +): string { + const preferScientific = user?.prefers_scientific_name_first === true + || user?.prefers_common_names === false; + return preferScientific + ? taxon.name + : ( taxon.preferredCommonName || taxon.name ); +} + // Translates rank in a way that can be statically checked export function translatedRank( rank: string, t: ( key: string ) => string ) { switch ( rank ) { From 5934d2321f821932b228ac35b71418aa0a18a09d Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Thu, 25 Jun 2026 10:43:24 -0700 Subject: [PATCH 07/14] fix offline results flicker on subsequent visits to search screen --- src/sharedHooks/useTaxonSearch.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/sharedHooks/useTaxonSearch.ts b/src/sharedHooks/useTaxonSearch.ts index 4cdde620a..2c3fe1d2d 100644 --- a/src/sharedHooks/useTaxonSearch.ts +++ b/src/sharedHooks/useTaxonSearch.ts @@ -39,7 +39,9 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => { const shouldFetchRemote = taxonQuery.length > 0; - const { data: remoteTaxa, refetch, isLoading } = useAuthenticatedQuery( + const { + data: remoteTaxa, refetch, isLoading, isFetched, + } = useAuthenticatedQuery( ["fetchTaxonSuggestions", taxonQuery], async ( optsWithAuth: ApiOpts ) => { const apiTaxa = await fetchSearchResults( @@ -96,6 +98,13 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => { if ( isLoading ) return; + // Don't fall back to local results until the remote query has actually + // run at least once. useAuthenticatedQuery starts with enabled=false + // while it resolves the auth state, which makes isLoading=false even + // though no remote fetch has happened yet — without this check we'd + // flash the "Showing offline search results" callout in that window. + if ( shouldFetchRemote && !isFetched ) return; + if ( remoteTaxa && remoteTaxa.length > 0 ) { if ( isSubscribed ) setLocalTaxa( null ); return; @@ -116,10 +125,12 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => { isSubscribed = false; }; }, [ + isFetched, isLoading, realm, remoteTaxa, safeRealmSearch, + shouldFetchRemote, taxonQuery, ] ); From ad725ea1991f7ac958573fd187e004269176398f Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Thu, 25 Jun 2026 11:13:55 -0700 Subject: [PATCH 08/14] reset should have disabled state and clear active search --- .../SearchScreens/ExploreSearchHeader.tsx | 21 +++++++++++++++---- .../Search/SearchMyObservationsTaxon.tsx | 7 ++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/Explore/SearchScreens/ExploreSearchHeader.tsx b/src/components/Explore/SearchScreens/ExploreSearchHeader.tsx index 130f3ee2e..74f8df5bf 100644 --- a/src/components/Explore/SearchScreens/ExploreSearchHeader.tsx +++ b/src/components/Explore/SearchScreens/ExploreSearchHeader.tsx @@ -1,9 +1,10 @@ +import classnames from "classnames"; import { BackButton, Body3, Heading4, } from "components/SharedComponents"; -import { View } from "components/styledComponents"; +import { Pressable, View } from "components/styledComponents"; import React from "react"; import { useTranslation } from "sharedHooks"; @@ -11,6 +12,7 @@ interface Props { closeModal: ( ) => void; headerText: string; resetFilters: ( ) => void; + resetDisabled?: boolean; testID: string; } @@ -18,6 +20,7 @@ const ExploreSearchHeader = ( { closeModal, headerText, resetFilters, + resetDisabled = false, testID, }: Props ) => { const { t } = useTranslation( ); @@ -32,11 +35,21 @@ const ExploreSearchHeader = ( { /> {headerText} - - + + {t( "Reset-verb" )} - + ); }; diff --git a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx index f4b58786e..462e98ca4 100644 --- a/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx +++ b/src/components/MyObservations/Search/SearchMyObservationsTaxon.tsx @@ -61,7 +61,11 @@ const SearchMyObservationsTaxon = ( ) => { closeScreen( ); }, [closeScreen, dispatch] ); - const resetSearch = useCallback( ( ) => setTaxonQuery( "" ), [] ); + const resetSearch = useCallback( ( ) => { + setTaxonQuery( "" ); + dispatch( { type: MY_OBSERVATIONS_ACTION.CLEAR_TAXON_SEARCH } ); + closeScreen( ); + }, [closeScreen, dispatch] ); const renderItem = useCallback( // no-unused-prop-types failing for components defined at runtime seems to @@ -86,6 +90,7 @@ const SearchMyObservationsTaxon = ( ) => { closeModal={closeScreen} headerText={t( "SEARCH" )} resetFilters={resetSearch} + resetDisabled={!searchedTaxon} testID="SearchMyObservationsTaxon.close" /> Date: Thu, 25 Jun 2026 11:52:49 -0700 Subject: [PATCH 09/14] supress announcements and banners while search is active --- src/components/MyObservations/SimpleHeader.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/MyObservations/SimpleHeader.tsx b/src/components/MyObservations/SimpleHeader.tsx index 6f8a531a8..ab8ed5883 100644 --- a/src/components/MyObservations/SimpleHeader.tsx +++ b/src/components/MyObservations/SimpleHeader.tsx @@ -4,6 +4,7 @@ import { INatIcon, } from "components/SharedComponents"; import { View } from "components/styledComponents"; +import { useMyObservations } from "providers/MyObservationsContext"; import React from "react"; import { useCurrentUser, useTranslation } from "sharedHooks"; import useStore from "stores/useStore"; @@ -37,6 +38,13 @@ const SimpleHeader = ( { const setIsDefaultMode = useStore( state => state.layout.setIsDefaultMode ); const currentUser = useCurrentUser(); + const { state: myObsState } = useMyObservations( ); + const searchActive = myObsState.searchedTaxon !== null; + + // While a search is active, the SearchedTaxonBanner is the only banner we + // want to show. Suppress the announcement / advanced-mode / location-missing + // banners so they don't compete for attention or change the layout. + if ( searchActive ) return null; const shouldShowAdvancedModeBanner = !!currentUser && isDefaultMode From b7b77eaf5f5bbda5265630688f3b10dc488670c9 Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Thu, 25 Jun 2026 20:21:36 -0700 Subject: [PATCH 10/14] add empty state --- .../Search/SearchEmptyState.tsx | 38 +++++++++++++++++++ src/i18n/l10n/en.ftl | 4 ++ src/i18n/l10n/en.ftl.json | 1 + src/i18n/strings.ftl | 4 ++ 4 files changed, 47 insertions(+) create mode 100644 src/components/MyObservations/Search/SearchEmptyState.tsx diff --git a/src/components/MyObservations/Search/SearchEmptyState.tsx b/src/components/MyObservations/Search/SearchEmptyState.tsx new file mode 100644 index 000000000..ab01ee652 --- /dev/null +++ b/src/components/MyObservations/Search/SearchEmptyState.tsx @@ -0,0 +1,38 @@ +// TODO: This component is intentionally not rendered anywhere yet. +// This is the empty state for the Search My Observations feature. + +import { + Body1, + Button, +} from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import { + MY_OBSERVATIONS_ACTION, + useMyObservations, +} from "providers/MyObservationsContext"; +import React from "react"; +import { useTranslation } from "sharedHooks"; + +const SearchEmptyState = ( ) => { + const { t } = useTranslation( ); + const { dispatch } = useMyObservations( ); + + return ( + + + {t( "Looks-like-you-havent-observed-this-yet-time-to-keep-exploring" )} + +