Merge pull request #3843 from inaturalist/user-posts

View a user's journal posts from their profile
This commit is contained in:
Johannes Klein
2026-07-17 16:15:22 +02:00
committed by GitHub
9 changed files with 154 additions and 39 deletions

View File

@@ -1,27 +1,34 @@
import type { ErrorWithResponse, INatApiError } from "api/error";
import handleError from "api/error";
import type {
ApiDefaultResult, ApiGetByIdParams, ApiOpts, ApiParams, ApiResponse,
} from "api/types";
import inatjs from "inaturalistjs";
const fetchUserPosts = async (
params: Record<string, unknown> = {},
opts: Record<string, unknown> = {},
): Promise<Record<string, unknown> | null | ErrorWithResponse | INatApiError> => {
const fetchBlogPosts = async <T = ApiDefaultResult>(
params: ApiParams = {},
opts: ApiOpts = {},
): Promise<ApiResponse<T> | null | ErrorWithResponse | INatApiError> => {
try {
return await inatjs.posts.for_user( params, opts );
const response = await inatjs.posts.for_user( params, opts );
if ( !response ) { return null; }
return response;
} catch ( e ) {
return handleError(
e as ErrorWithResponse,
{ context: { functionName: "fetchUserPosts", opts } },
{ context: { functionName: "fetchBlogPosts", opts } },
);
}
};
const fetchProjectPosts = async (
params: Record<string, unknown> = {},
opts: Record<string, unknown> = {},
): Promise<Record<string, unknown> | null | ErrorWithResponse | INatApiError> => {
const fetchProjectPosts = async <T = ApiDefaultResult>(
params: ApiGetByIdParams,
opts: ApiOpts = {},
): Promise<ApiResponse<T> | null | ErrorWithResponse | INatApiError> => {
try {
return await inatjs.projects.posts( params, opts );
const response = await inatjs.projects.posts( params, opts );
if ( !response ) { return null; }
return response;
} catch ( e ) {
return handleError(
e as ErrorWithResponse,
@@ -30,7 +37,24 @@ const fetchProjectPosts = async (
}
};
const fetchUserPosts = async <T = ApiDefaultResult>(
params: ApiGetByIdParams,
opts: ApiOpts = {},
): Promise<ApiResponse<T> | null | ErrorWithResponse | INatApiError> => {
try {
const response = await inatjs.users.posts( params, opts );
if ( !response ) { return null; }
return response;
} catch ( e ) {
return handleError(
e as ErrorWithResponse,
{ context: { functionName: "fetchUserPosts", opts } },
);
}
};
export {
fetchBlogPosts,
fetchProjectPosts,
fetchUserPosts,
};

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

@@ -14,6 +14,10 @@ export interface ApiParams {
ttl?: number;
}
export interface ApiGetByIdParams extends ApiParams {
id: number;
}
export interface ApiPlace {
id?: number;
name?: string;
@@ -21,7 +25,7 @@ export interface ApiPlace {
place_type?: number | null;
}
export interface ApiPostForProject {
export interface ApiPost {
body: string;
id: number;
published_at: string;
@@ -29,7 +33,7 @@ export interface ApiPostForProject {
}
// When using POST_FOR_USER_FIELDS
export interface ApiPostForUser extends ApiPostForProject {
export interface ApiPostForUser extends ApiPost {
parent: {
id: number;
icon_url: string | null;

View File

@@ -1,17 +1,12 @@
import type { ErrorWithResponse, INatApiError } from "api/error";
import handleError from "api/error";
import type {
ApiDefaultResult, ApiGetByIdParams, ApiOpts, ApiResponse,
} from "api/types";
import inatjs from "inaturalistjs";
import type { ErrorWithResponse, INatApiError } from "./error";
import handleError from "./error";
import type {
ApiDefaultResult, ApiOpts, ApiParams, ApiResponse,
} from "./types";
interface UsersProjectsParams extends ApiParams {
id: number;
}
const fetchUserProjects = async <T = ApiDefaultResult>(
params: UsersProjectsParams,
params: ApiGetByIdParams,
opts: ApiOpts = {},
): Promise<ApiResponse<T> | null | ErrorWithResponse | INatApiError> => {
try {

View File

@@ -1,6 +1,6 @@
import { useNavigation } from "@react-navigation/native";
import { POST_FOR_USER_FIELDS } from "api/fields";
import { fetchUserPosts } from "api/posts";
import { fetchBlogPosts } from "api/posts";
import { ScreenShell } from "components/SharedComponents/ViewWrapper";
import type { TabStackScreenProps } from "navigation/types";
import React, {
@@ -18,7 +18,7 @@ const Blog = ( ) => {
const navigation = useNavigation<TabStackScreenProps<"Journal">["navigation"]>( );
const { t } = useTranslation( );
const queryKey = ["fetchUserPosts"];
const queryKey = ["fetchBlogPosts"];
const queryParams = {
fields: POST_FOR_USER_FIELDS,
};
@@ -28,7 +28,7 @@ const Blog = ( ) => {
fetchNextPage,
isFetchingNextPage,
totalResults: totalPosts,
} = useInfiniteScroll( queryKey, fetchUserPosts, queryParams, {
} = useInfiniteScroll( queryKey, fetchBlogPosts, queryParams, {
enabled: true,
} );

View File

@@ -4,11 +4,12 @@ import React from "react";
import Blog from "./Blog";
import ProjectPosts from "./ProjectPosts";
import UserPosts from "./UserPosts";
const Journal = ( ) => {
const { params } = useRoute<TabStackScreenProps<"Journal">["route"]>( );
const {
journalPostsCount, projectIcon, projectId, projectTitle, userLogin,
projectIcon, projectId, projectTitle, userIcon, userId, userLogin,
} = params || {};
if ( projectId ) {
@@ -21,10 +22,14 @@ const Journal = ( ) => {
);
}
// TODO: posts for one user
if ( userLogin ) {
console.log( journalPostsCount );
return null;
if ( userId ) {
return (
<UserPosts
userIcon={userIcon}
userId={userId}
userLogin={userLogin}
/>
);
}
return (

View File

@@ -16,11 +16,11 @@ import PostList from "./PostList";
interface Props {
projectIcon?: string;
projectId?: number;
projectId: number;
projectTitle?: string;
}
const PostsForProjects = ( {
const ProjectPosts = ( {
projectIcon,
projectId,
projectTitle,
@@ -81,4 +81,4 @@ const PostsForProjects = ( {
);
};
export default PostsForProjects;
export default ProjectPosts;

View File

@@ -0,0 +1,84 @@
import { useNavigation } from "@react-navigation/native";
import { POST_FOR_PROJECT_FIELDS } from "api/fields";
import { fetchUserPosts } from "api/posts";
import { ScreenShell } from "components/SharedComponents/ViewWrapper";
import type { TabStackScreenProps } from "navigation/types";
import React, {
useEffect,
useMemo,
} from "react";
import {
useInfiniteScroll,
useTranslation,
} from "sharedHooks";
import PostList from "./PostList";
interface Props {
userIcon?: string;
userId: number;
userLogin?: string;
}
const UserPosts = ( {
userIcon,
userId,
userLogin,
}: Props ) => {
const navigation
= useNavigation<TabStackScreenProps<"Journal">["navigation"]>();
const { t } = useTranslation();
const queryKey = ["fetchUserPosts", userId];
const queryParams = {
id: userId,
fields: POST_FOR_PROJECT_FIELDS,
};
const {
data: userPosts,
fetchNextPage,
isFetchingNextPage,
totalResults: totalPosts,
} = useInfiniteScroll( queryKey, fetchUserPosts, queryParams, {
enabled: !!userId,
} );
const headerOptions = useMemo(
() => ( {
headerTitle: userLogin,
headerSubtitle: t( "X-JOURNAL_POSTS", {
count: totalPosts || 0,
} ),
} ),
[totalPosts, t, userLogin],
);
useEffect( () => {
navigation.setOptions( headerOptions );
}, [headerOptions, navigation] );
const enrichedPosts = useMemo( () => {
if ( !userPosts ) return null;
return userPosts?.map( p => ( {
...p,
parent: {
id: userId,
icon_url: userIcon,
},
} ) );
}, [userIcon, userId, userPosts] );
return (
<ScreenShell>
<PostList
posts={enrichedPosts}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
</ScreenShell>
);
};
export default UserPosts;

View File

@@ -146,8 +146,9 @@ const UserProfile = ( ) => {
const onJournalPostsPressed = ( ) => {
navigation.navigate( "Journal", {
userIcon: User.uri( user ),
userId: user?.id,
userLogin: user?.login,
journalPostsCount: user?.journal_posts_count,
} );
};

View File

@@ -327,8 +327,9 @@ export type BaseTabStackParamList = {
};
// From UserProfile
// {
// userId: user?.id,
// userIcon: user?.icon_url,
// userLogin: user?.login,
// journalPostsCount: user?.journal_posts_count,
// }
// From ProjectDetails
// {
@@ -337,11 +338,12 @@ export type BaseTabStackParamList = {
// projectTitle: project?.title,
// }
Journal: {
userLogin?: string;
projectIcon?: string;
projectId?: number;
projectTitle?: string;
journalPostsCount?: number;
userIcon?: string;
userId?: number;
userLogin?: string;
} | undefined;
Debug: undefined;
UILibrary: undefined;