Standardize offline notice, empty results, and loading for search screens; closes #1925 (#1982)

This commit is contained in:
Amanda Bullington
2024-08-16 13:12:24 -07:00
committed by GitHub
parent 62dce20bd9
commit a0fd52ca27
8 changed files with 144 additions and 115 deletions

View File

@@ -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 (
<View className="pt-[50px]">
<OfflineNotice onPress={refetch} />
</View>
);
}
if ( isLoading ) {
return (
<View className="p-4">
<ActivityIndicator size={40} />
</View>
);
}
return (
<Body2 className="text-center pt-[50px]">{t( "No-results-found-for-that-search" )}</Body2>
);
};
export default EmptySearchResults;

View File

@@ -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 = ( ) => (
<EmptySearchResults
isLoading={isLoading}
searchQuery={locationName}
refetch={refetch}
/>
);
return (
<ViewWrapper testID="explore-location-search">
<View className="flex-row justify-center p-5 bg-white">
<INatIconButton
testID="ExploreTaxonSearch.close"
testID="ExploreLocationSearch.close"
size={18}
icon="back"
className="absolute top-2 left-3 z-10"
@@ -138,7 +147,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
<SearchBar
handleTextChange={locationText => setLocationName( locationText )}
value={locationName}
testID="LocationPicker.locationSearch"
testID="ExploreLocationSearch.locationSearch"
/>
</View>
<View className="flex-row px-3 mt-5 justify-evenly">
@@ -155,20 +164,13 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
/>
</View>
</View>
{isLoading
? (
<View className="p-4">
<ActivityIndicator size={40} />
</View>
)
: (
<FlatList
keyboardShouldPersistTaps="always"
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
)}
<FlatList
keyboardShouldPersistTaps="always"
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
ListEmptyComponent={renderEmptyList}
/>
{renderPermissionsGate( { onPermissionGranted: setNearbyLocation } )}
</ViewWrapper>
);

View File

@@ -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 => {
<View className="border-b border-lightGray" />
);
const renderEmptyList = ( ) => (
<EmptySearchResults
isLoading={isLoading}
searchQuery={projectQuery}
refetch={refetch}
/>
);
return (
<ViewWrapper>
<View className="flex-row justify-center p-5 bg-white">
<INatIconButton
testID="ExploreTaxonSearch.close"
testID="ExploreProjectSearch.close"
size={18}
icon="back"
className="absolute top-2 left-3 z-10"
@@ -98,30 +107,23 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
style={DROP_SHADOW}
>
<SearchBar
handleTextChange={setUserQuery}
value={userQuery}
testID="SearchUser"
handleTextChange={setProjectQuery}
value={projectQuery}
testID="SearchProject"
/>
</View>
{isLoading
? (
<View className="p-4">
<ActivityIndicator size={40} />
</View>
)
: (
<FlashList
data={projects}
initialNumToRender={5}
estimatedItemSize={100}
testID="SearchUserList"
keyExtractor={item => item.id}
renderItem={renderItem}
ListHeaderComponent={renderItemSeparator}
ItemSeparatorComponent={renderItemSeparator}
accessible
/>
)}
<FlashList
data={projects}
initialNumToRender={5}
estimatedItemSize={100}
testID="SearchProjectList"
keyExtractor={item => item.id}
renderItem={renderItem}
ListEmptyComponent={renderEmptyList}
ListHeaderComponent={renderItemSeparator}
ItemSeparatorComponent={renderItemSeparator}
accessible
/>
</ViewWrapper>
);
};

View File

@@ -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 => {
<View className="border-b border-lightGray" />
);
const renderEmptyList = ( ) => (
<EmptySearchResults
isLoading={isLoading}
searchQuery={userQuery}
refetch={refetch}
/>
);
return (
<ViewWrapper>
<View className="flex-row justify-center p-5 bg-white">
<INatIconButton
testID="ExploreTaxonSearch.close"
testID="ExploreUserSearch.close"
size={18}
icon="back"
className="absolute top-2 left-3 z-10"
@@ -108,26 +117,19 @@ const ExploreUserSearch = ( { closeModal, updateUser }: Props ): Node => {
testID="SearchUser"
/>
</View>
{isLoading
? (
<View className="p-4">
<ActivityIndicator size={40} />
</View>
)
: (
<FlashList
ItemSeparatorComponent={renderItemSeparator}
ListHeaderComponent={renderItemSeparator}
accessible
data={userList}
estimatedItemSize={100}
initialNumToRender={5}
keyExtractor={item => item.id}
keyboardShouldPersistTaps="handled"
renderItem={renderItem}
testID="SearchUserList"
/>
)}
<FlashList
ItemSeparatorComponent={renderItemSeparator}
ListEmptyComponent={renderEmptyList}
ListHeaderComponent={renderItemSeparator}
accessible
data={userList}
estimatedItemSize={100}
initialNumToRender={5}
keyExtractor={item => item.id}
keyboardShouldPersistTaps="handled"
renderItem={renderItem}
testID="SearchUserList"
/>
</ViewWrapper>
);
};

View File

@@ -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 (
<View className="p-4">
<ActivityIndicator size={40} />
</View>
);
}
const showIfOffline = taxonQuery.length > 0 && (
!taxa || ( taxa instanceof Array && taxa.length === 0 )
);
if ( showIfOffline && !isConnected ) {
return (
<View className="p-4">
<OfflineNotice
onPress={() => {
refresh();
refetch();
}}
/>
</View>
);
}
return (
<FlatList
keyboardShouldPersistTaps="always"
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
);
};
const renderEmptyList = ( ) => (
<EmptySearchResults
isLoading={isLoading}
searchQuery={taxonQuery}
refetch={refetch}
/>
);
return (
<View className="flex-1">
{renderMainContent()}
</View>
<FlatList
keyboardShouldPersistTaps="always"
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
ListEmptyComponent={renderEmptyList}
/>
);
};

View File

@@ -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

View File

@@ -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)"

View File

@@ -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