feat: show all results in Explore projects search (#2621)

* refactor: convert ExploreProjectSearch to TS and assoc'd TS changes
* feat: use infinite scroll in Explore Filters project search
* feat: show universal search results in Explore project search

This should show the same ranked search results a user would see at
https://www.inaturalist.org/search?source[]=projects

Closes MOB-313
This commit is contained in:
Ken-ichi
2025-01-17 16:31:07 -08:00
committed by GitHub
parent 9872a99f47
commit 96c316a257
16 changed files with 79 additions and 57 deletions

View File

@@ -1526,7 +1526,7 @@ SPEC CHECKSUMS:
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e
RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5
RCTTypeSafety: ede1e2576424d89471ef553b2aed09fbbcc038e3
React: 2ddb437e599df2f1bffa9b248de2de4cfa0227f0

View File

@@ -33,10 +33,11 @@ const PARAMS: ApiParams = {
fields: "all"
};
const fetchSearchResults = async (
// Vanilla search wrapper with error handling
const search = async (
params: SearchParams = {},
opts: ApiOpts = {}
): Promise<null | ( ApiPlace | ApiProject | ApiTaxon | ApiUser )[]> => {
): Promise<null | SearchResponse> => {
let response: SearchResponse;
try {
response = await inatjs.search( { ...PARAMS, ...params }, opts );
@@ -46,6 +47,15 @@ const fetchSearchResults = async (
// this is just to placate typescript
return null;
}
return response;
};
// Hits /search AND maps results so it just returns and array of results
const fetchSearchResults = async (
params: SearchParams = {},
opts: ApiOpts = {}
): Promise<null | ( ApiPlace | ApiProject | ApiTaxon | ApiUser )[]> => {
const response = await search( params, opts );
if ( !response ) { return null; }
const sources = [params.sources].flat();
const records: ( ApiPlace | ApiProject | ApiTaxon | ApiUser )[] = [];
@@ -65,4 +75,4 @@ const fetchSearchResults = async (
return records;
};
export default fetchSearchResults;
export { fetchSearchResults, search };

2
src/api/types.d.ts vendored
View File

@@ -16,7 +16,9 @@ export interface ApiPlace {
}
export interface ApiProject {
icon?: string;
id?: number;
project_type?: "collection" | "umbrella" | ""; // FYI "" means "traditional"
title?: string;
}

View File

@@ -1,12 +1,12 @@
import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjectSearch";
import type { ApiProject } from "api/types";
import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjectSearch.tsx";
import Modal from "components/SharedComponents/Modal.tsx";
import React from "react";
interface Props {
showModal: boolean;
closeModal: () => void;
// TODO: Param not typed yet, because ExploreProjectSearch is not typed yet
updateProject: ( project: null | { title: string } ) => void;
updateProject: ( project: ApiProject ) => void;
}
const ExploreProjectSearchModal = ( {

View File

@@ -1,3 +1,4 @@
import type { ApiProject } from "api/types";
import classNames from "classnames";
import NumberBadge from "components/Explore/NumberBadge.tsx";
import ProjectListItem from "components/ProjectList/ProjectListItem.tsx";
@@ -68,8 +69,7 @@ interface Props {
updateLocation: ( location: "worldwide" | { name: string } ) => void;
// TODO: Param not typed yet, because ExploreUserSearch is not typed yet
updateUser: ( user: null | { login: string } ) => void;
// TODO: Param not typed yet, because ExploreProjectSearch is not typed yet
updateProject: ( project: null | { title: string } ) => void;
updateProject: ( project: ApiProject ) => void;
}
const FilterModal = ( {

View File

@@ -1,6 +1,6 @@
// @flow
import fetchSearchResults from "api/search.ts";
import { fetchSearchResults } from "api/search.ts";
import {
Body1,
ButtonBar,

View File

@@ -1,18 +1,16 @@
// @flow
import { searchProjects } from "api/projects";
import { search } from "api/search.ts";
import type { ApiProject } from "api/types";
import ProjectList from "components/ProjectList/ProjectList.tsx";
import {
SearchBar,
ViewWrapper
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, {
useCallback,
useState
} from "react";
import { useAuthenticatedQuery, useTranslation } from "sharedHooks";
import { useInfiniteScroll, useTranslation } from "sharedHooks";
import { getShadow } from "styles/global";
import EmptySearchResults from "./EmptySearchResults";
@@ -23,22 +21,41 @@ const DROP_SHADOW = getShadow( {
} );
type Props = {
closeModal: Function,
updateProject: Function
closeModal: ( ) => void,
updateProject: ( project: ApiProject ) => void
};
const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
const ExploreProjectSearch = ( { closeModal, updateProject }: Props ) => {
const [projectQuery, setProjectQuery] = useState( "" );
const { t } = useTranslation();
const { data, isLoading, refetch } = useAuthenticatedQuery(
["searchProjects", projectQuery],
optsWithAuth => searchProjects( { q: projectQuery }, optsWithAuth )
// TODO fix these types if/when we ever figure out how to type react query
// wrappers like useInfiniteScroll
const {
data,
isFetching,
fetchNextPage,
refetch
} = useInfiniteScroll(
["ExploreProjectSearch", projectQuery],
search,
{
q: projectQuery,
sources: "projects",
fields: {
project: {
id: true,
title: true,
icon: true,
project_type: true
}
}
}
);
const projects = data?.results;
const projects = data.map( ( r: { project: ApiProject } ) => r.project );
const onProjectSelected = useCallback( async project => {
const onProjectSelected = useCallback( async ( project: ApiProject ) => {
if ( !project.id ) {
// If this is missing, we can not query by project
// TODO: user facing error message
@@ -56,16 +73,6 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
[updateProject, closeModal]
);
const renderEmptyList = ( ) => (
<EmptySearchResults
isLoading={isLoading}
searchQuery={projectQuery}
refetch={refetch}
/>
);
const renderFooter = ( ) => <View className="h-[336px]" />;
return (
<ViewWrapper>
<ExploreSearchHeader
@@ -86,8 +93,15 @@ const ExploreProjectSearch = ( { closeModal, updateProject }: Props ): Node => {
</View>
<ProjectList
projects={projects}
ListFooterComponent={renderFooter}
ListEmptyCompoent={renderEmptyList}
ListFooterComponent={<View className="h-[336px]" />}
ListEmptyComponent={(
<EmptySearchResults
isLoading={isFetching}
searchQuery={projectQuery}
refetch={refetch}
/>
)}
onEndReached={fetchNextPage}
onPress={onProjectSelected}
accessibilityLabel={t( "Change-project" )}
/>

View File

@@ -1,6 +1,6 @@
// @flow
import fetchSearchResults from "api/search.ts";
import { fetchSearchResults } from "api/search.ts";
import {
ButtonBar,
SearchBar,

View File

@@ -1,5 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
import fetchSearchResults from "api/search.ts";
import { fetchSearchResults } from "api/search.ts";
import type { ApiOpts } from "api/types.d.ts";
import {
Body3,

View File

@@ -137,8 +137,6 @@ const ProjectDetails = ( {
);
};
console.log( project?.header_image_url, "header" );
const backgroundImageSource = project?.header_image_url
? { uri: project.header_image_url }
: require( "images/background/project_banner.jpg" );

View File

@@ -1,4 +1,5 @@
import { useNavigation } from "@react-navigation/native";
import type { ApiProject } from "api/types";
import {
CustomFlashList
} from "components/SharedComponents";
@@ -15,7 +16,7 @@ interface Props {
ListEmptyComponent?: React.JSX.Element
ListFooterComponent?: React.JSX.Element
onEndReached?: ( ) => void
onPress?: ( ) => void
onPress?: ( project: ApiProject ) => void
accessibilityLabel?: string
}
@@ -30,7 +31,7 @@ const ProjectList = ( {
const navigation = useNavigation( );
const { t } = useTranslation( );
const renderProject = ( { item: project } ) => (
const renderProject = ( { item: project }: { item: ApiProject } ) => (
<Pressable
className="px-4 py-1.5"
onPress={( ) => {

View File

@@ -1,3 +1,4 @@
import type { ApiProject } from "api/types";
import classnames from "classnames";
import displayProjectType from "components/Projects/helpers/displayProjectType.ts";
import {
@@ -15,12 +16,7 @@ import formatProjectDate from "../Projects/helpers/displayDates";
const defaultProjectIcon = "https://www.inaturalist.org/attachment_defaults/general/span2.png";
type Props = {
item: {
id: string;
icon: string;
title: string;
project_type: string;
} | undefined | null;
item?: ApiProject | null;
isHeader?: boolean;
};

View File

@@ -12,7 +12,7 @@ import Donate from "components/Donate/Donate.tsx";
import ExploreContainer from "components/Explore/ExploreContainer";
import RootExploreContainer from "components/Explore/RootExploreContainer";
import ExploreLocationSearch from "components/Explore/SearchScreens/ExploreLocationSearch";
import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjectSearch";
import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjectSearch.tsx";
import ExploreTaxonSearch from "components/Explore/SearchScreens/ExploreTaxonSearch";
import ExploreUserSearch from "components/Explore/SearchScreens/ExploreUserSearch";
import Help from "components/Help/Help.tsx";

View File

@@ -24,7 +24,7 @@ const useAuthenticatedInfiniteQuery = (
retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, {
queryKey
} ),
initialPageParam: 0,
initialPageParam: 1,
...queryOptions
} );

View File

@@ -1,13 +1,12 @@
// @flow
import { flatten } from "lodash";
import { useAuthenticatedInfiniteQuery } from "sharedHooks";
const useInfiniteScroll = (
queryKey: string,
apiCall: Function,
newInputParams: Object,
options: {
options?: {
enabled: boolean
}
): Object => {
@@ -22,10 +21,11 @@ const useInfiniteScroll = (
isFetching,
isFetchingNextPage,
fetchNextPage,
refetch,
status
} = useAuthenticatedInfiniteQuery(
[queryKey, baseParams],
async ( { pageParam = 0 }, optsWithAuth ) => {
async ( { pageParam = 1 }, optsWithAuth ) => {
const params = {
...baseParams
};
@@ -38,18 +38,19 @@ const useInfiniteScroll = (
getNextPageParam: lastPage => ( lastPage
? lastPage.page + 1
: 1 ),
enabled: options.enabled
enabled: options?.enabled
}
);
const pages = data?.pages;
const allResults = pages?.map( page => page?.results );
const pages = data?.pages || [];
const allResults = pages?.map( page => page?.results ).flat( Infinity ).filter( Boolean );
return {
data: flatten( allResults ),
data: allResults,
isFetching,
isFetchingNextPage,
fetchNextPage,
refetch,
status,
totalResults: pages?.[0]
? pages?.[0].total_results

View File

@@ -1,4 +1,4 @@
import fetchSearchResults from "api/search.ts";
import { fetchSearchResults } from "api/search.ts";
import type { ApiOpts } from "api/types";
import { RealmContext } from "providers/contexts.ts";
import { useEffect } from "react";