From a0fd52ca274599f30540a70d7911027f35d50e4f Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:12:24 -0700 Subject: [PATCH] Standardize offline notice, empty results, and loading for search screens; closes #1925 (#1982) --- .../SearchScreens/EmptySearchResults.tsx | 45 +++++++++++++ .../SearchScreens/ExploreLocationSearch.js | 38 ++++++----- .../SearchScreens/ExploreProjectSearch.js | 58 ++++++++-------- .../SearchScreens/ExploreUserSearch.js | 48 ++++++------- .../SharedComponents/TaxaList/TaxaList.tsx | 67 ++++++------------- src/i18n/l10n/en.ftl | 1 + src/i18n/l10n/en.ftl.json | 1 + src/i18n/strings.ftl | 1 + 8 files changed, 144 insertions(+), 115 deletions(-) create mode 100644 src/components/Explore/SearchScreens/EmptySearchResults.tsx diff --git a/src/components/Explore/SearchScreens/EmptySearchResults.tsx b/src/components/Explore/SearchScreens/EmptySearchResults.tsx new file mode 100644 index 000000000..2c413ab7e --- /dev/null +++ b/src/components/Explore/SearchScreens/EmptySearchResults.tsx @@ -0,0 +1,45 @@ +import { useNetInfo } from "@react-native-community/netinfo"; +import { + ActivityIndicator, + Body2, + OfflineNotice +} from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import React from "react"; +import { + useTranslation +} from "sharedHooks"; + +interface Props { + isLoading: boolean, + searchQuery: string, + refetch: Function +} + +const EmptySearchResults = ( { isLoading, searchQuery, refetch }: Props ) => { + const { t } = useTranslation( ); + const { isConnected } = useNetInfo( ); + + if ( searchQuery === "" ) { + return null; + } + if ( isConnected === false ) { + return ( + + + + ); + } + if ( isLoading ) { + return ( + + + + ); + } + return ( + {t( "No-results-found-for-that-search" )} + ); +}; + +export default EmptySearchResults; diff --git a/src/components/Explore/SearchScreens/ExploreLocationSearch.js b/src/components/Explore/SearchScreens/ExploreLocationSearch.js index 31ee2cc6f..3bdd4abc4 100644 --- a/src/components/Explore/SearchScreens/ExploreLocationSearch.js +++ b/src/components/Explore/SearchScreens/ExploreLocationSearch.js @@ -2,7 +2,6 @@ import fetchSearchResults from "api/search"; import { - ActivityIndicator, Body3, Button, Heading4, @@ -26,6 +25,8 @@ import { useAuthenticatedQuery, useTranslation } from "sharedHooks"; import useLocationPermission from "sharedHooks/useLocationPermission.tsx"; import { getShadow } from "styles/global"; +import EmptySearchResults from "./EmptySearchResults"; + const DROP_SHADOW = getShadow( { offsetHeight: 4 } ); @@ -51,7 +52,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node => [updateLocation, closeModal] ); - const { data: placeResults, isLoading } = useAuthenticatedQuery( + const { data: placeResults, isLoading, refetch } = useAuthenticatedQuery( ["fetchSearchResults", locationName], optsWithAuth => fetchSearchResults( { @@ -114,11 +115,19 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node => } }; + const renderEmptyList = ( ) => ( + + ); + return ( setLocationName( locationText )} value={locationName} - testID="LocationPicker.locationSearch" + testID="ExploreLocationSearch.locationSearch" /> @@ -155,20 +164,13 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node => /> - {isLoading - ? ( - - - - ) - : ( - item.id} - /> - )} + item.id} + ListEmptyComponent={renderEmptyList} + /> {renderPermissionsGate( { onPermissionGranted: setNearbyLocation } )} ); diff --git a/src/components/Explore/SearchScreens/ExploreProjectSearch.js b/src/components/Explore/SearchScreens/ExploreProjectSearch.js index a53b61efe..6b4e63f4d 100644 --- a/src/components/Explore/SearchScreens/ExploreProjectSearch.js +++ b/src/components/Explore/SearchScreens/ExploreProjectSearch.js @@ -3,7 +3,6 @@ import { FlashList } from "@shopify/flash-list"; import { searchProjects } from "api/projects"; import { - ActivityIndicator, Body3, Heading4, INatIconButton, @@ -20,6 +19,8 @@ import React, { import { useAuthenticatedQuery, useTranslation } from "sharedHooks"; import { getShadow } from "styles/global"; +import EmptySearchResults from "./EmptySearchResults"; + const DROP_SHADOW = getShadow( { offsetHeight: 4 } ); @@ -30,12 +31,12 @@ type Props = { }; const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => { - const [userQuery, setUserQuery] = useState( "" ); + const [projectQuery, setProjectQuery] = useState( "" ); const { t } = useTranslation(); - const { data: projects, isLoading } = useAuthenticatedQuery( - ["searchProjects", userQuery], - optsWithAuth => searchProjects( { q: userQuery }, optsWithAuth ) + const { data: projects, isLoading, refetch } = useAuthenticatedQuery( + ["searchProjects", projectQuery], + optsWithAuth => searchProjects( { q: projectQuery }, optsWithAuth ) ); const onProjectSelected = useCallback( async project => { @@ -77,11 +78,19 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => { ); + const renderEmptyList = ( ) => ( + + ); + return ( { style={DROP_SHADOW} > - {isLoading - ? ( - - - - ) - : ( - item.id} - renderItem={renderItem} - ListHeaderComponent={renderItemSeparator} - ItemSeparatorComponent={renderItemSeparator} - accessible - /> - )} + item.id} + renderItem={renderItem} + ListEmptyComponent={renderEmptyList} + ListHeaderComponent={renderItemSeparator} + ItemSeparatorComponent={renderItemSeparator} + accessible + /> ); }; diff --git a/src/components/Explore/SearchScreens/ExploreUserSearch.js b/src/components/Explore/SearchScreens/ExploreUserSearch.js index 2330b564f..17cfe84fe 100644 --- a/src/components/Explore/SearchScreens/ExploreUserSearch.js +++ b/src/components/Explore/SearchScreens/ExploreUserSearch.js @@ -3,7 +3,6 @@ import { FlashList } from "@shopify/flash-list"; import fetchSearchResults from "api/search"; import { - ActivityIndicator, Body3, Heading4, INatIconButton, @@ -20,6 +19,8 @@ import React, { import { useAuthenticatedQuery, useTranslation } from "sharedHooks"; import { getShadow } from "styles/global"; +import EmptySearchResults from "./EmptySearchResults"; + const DROP_SHADOW = getShadow( { offsetHeight: 4 } ); @@ -34,7 +35,7 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => { const { t } = useTranslation(); // TODO: replace this with infinite scroll like ExploreFlashList - const { data: userList, isLoading } = useAuthenticatedQuery( + const { data: userList, isLoading, refetch } = useAuthenticatedQuery( ["fetchSearchResults", userQuery], optsWithAuth => fetchSearchResults( { @@ -82,11 +83,19 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => { ); + const renderEmptyList = ( ) => ( + + ); + return ( { testID="SearchUser" /> - {isLoading - ? ( - - - - ) - : ( - item.id} - keyboardShouldPersistTaps="handled" - renderItem={renderItem} - testID="SearchUserList" - /> - )} + item.id} + keyboardShouldPersistTaps="handled" + renderItem={renderItem} + testID="SearchUserList" + /> ); }; diff --git a/src/components/SharedComponents/TaxaList/TaxaList.tsx b/src/components/SharedComponents/TaxaList/TaxaList.tsx index 1cd34f9c5..b90d338f8 100644 --- a/src/components/SharedComponents/TaxaList/TaxaList.tsx +++ b/src/components/SharedComponents/TaxaList/TaxaList.tsx @@ -1,6 +1,4 @@ -import { refresh, useNetInfo } from "@react-native-community/netinfo"; -import { ActivityIndicator, OfflineNotice } from "components/SharedComponents"; -import { View } from "components/styledComponents"; +import EmptySearchResults from "components/Explore/SearchScreens/EmptySearchResults.tsx"; import React from "react"; import { FlatList } from "react-native"; import { useIconicTaxa } from "sharedHooks"; @@ -24,53 +22,30 @@ const TaxaList = ( { }: Props ) => { // TODO: how to use Realm with TS const iconicTaxa = useIconicTaxa( { reload: false } ); - const { isConnected } = useNetInfo( ); - let data = iconicTaxa; - if ( taxa && taxa.length > 0 ) { - // TODO: how to use Realm with TS - data = taxa; - } + // 20240816 amanda - afaik we only want to seed the initial screen + // with iconic taxon data, and we still want to be able to show the empty screen + // and offline state when a user has typed in a query + const data = taxonQuery === "" + ? iconicTaxa + : taxa; - const renderMainContent = () => { - if ( isLoading ) { - return ( - - - - ); - } - - const showIfOffline = taxonQuery.length > 0 && ( - !taxa || ( taxa instanceof Array && taxa.length === 0 ) - ); - if ( showIfOffline && !isConnected ) { - return ( - - { - refresh(); - refetch(); - }} - /> - - ); - } - - return ( - item.id} - /> - ); - }; + const renderEmptyList = ( ) => ( + + ); return ( - - {renderMainContent()} - + item.id} + ListEmptyComponent={renderEmptyList} + /> ); }; diff --git a/src/i18n/l10n/en.ftl b/src/i18n/l10n/en.ftl index 82150695e..d92237069 100644 --- a/src/i18n/l10n/en.ftl +++ b/src/i18n/l10n/en.ftl @@ -603,6 +603,7 @@ No-Notifications-Found = You have no notifications! Get started by creating your No-projects-match-that-search = No projects match that search # Used for explore screen when search params lead to a search with no data No-results-found = No results found +No-results-found-for-that-search = No results found for that search. # license code no-rights-reserved-cc0 = no rights reserved (CC0) NONE = NONE diff --git a/src/i18n/l10n/en.ftl.json b/src/i18n/l10n/en.ftl.json index 3334bfd5e..927c34926 100644 --- a/src/i18n/l10n/en.ftl.json +++ b/src/i18n/l10n/en.ftl.json @@ -812,6 +812,7 @@ "comment": "Used for explore screen when search params lead to a search with no data", "val": "No results found" }, + "No-results-found-for-that-search": "No results found for that search.", "no-rights-reserved-cc0": { "comment": "license code", "val": "no rights reserved (CC0)" diff --git a/src/i18n/strings.ftl b/src/i18n/strings.ftl index 82150695e..d92237069 100644 --- a/src/i18n/strings.ftl +++ b/src/i18n/strings.ftl @@ -603,6 +603,7 @@ No-Notifications-Found = You have no notifications! Get started by creating your No-projects-match-that-search = No projects match that search # Used for explore screen when search params lead to a search with no data No-results-found = No results found +No-results-found-for-that-search = No results found for that search. # license code no-rights-reserved-cc0 = no rights reserved (CC0) NONE = NONE