mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
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:
@@ -1526,7 +1526,7 @@ SPEC CHECKSUMS:
|
||||
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
|
||||
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
|
||||
Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e
|
||||
RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
|
||||
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
|
||||
RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5
|
||||
RCTTypeSafety: ede1e2576424d89471ef553b2aed09fbbcc038e3
|
||||
React: 2ddb437e599df2f1bffa9b248de2de4cfa0227f0
|
||||
|
||||
@@ -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
2
src/api/types.d.ts
vendored
@@ -16,7 +16,9 @@ export interface ApiPlace {
|
||||
}
|
||||
|
||||
export interface ApiProject {
|
||||
icon?: string;
|
||||
id?: number;
|
||||
project_type?: "collection" | "umbrella" | ""; // FYI "" means "traditional"
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ( {
|
||||
|
||||
@@ -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 = ( {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import fetchSearchResults from "api/search.ts";
|
||||
import { fetchSearchResults } from "api/search.ts";
|
||||
import {
|
||||
Body1,
|
||||
ButtonBar,
|
||||
|
||||
@@ -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" )}
|
||||
/>
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import fetchSearchResults from "api/search.ts";
|
||||
import { fetchSearchResults } from "api/search.ts";
|
||||
import {
|
||||
ButtonBar,
|
||||
SearchBar,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" );
|
||||
|
||||
@@ -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={( ) => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -24,7 +24,7 @@ const useAuthenticatedInfiniteQuery = (
|
||||
retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, {
|
||||
queryKey
|
||||
} ),
|
||||
initialPageParam: 0,
|
||||
initialPageParam: 1,
|
||||
...queryOptions
|
||||
} );
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user