diff --git a/web/src/components/MemoCommentSection.tsx b/web/src/components/MemoCommentSection.tsx index 76a70d8d7..379157086 100644 --- a/web/src/components/MemoCommentSection.tsx +++ b/web/src/components/MemoCommentSection.tsx @@ -1,4 +1,4 @@ -import { MessageCircleIcon } from "lucide-react"; +import { LoaderCircleIcon, MessageCircleIcon } from "lucide-react"; import { useState } from "react"; import MemoEditor from "@/components/MemoEditor"; import MemoView from "@/components/MemoView"; @@ -12,9 +12,12 @@ interface Props { memo: Memo; comments: Memo[]; parentPage?: string; + hasMoreComments?: boolean; + isFetchingMoreComments?: boolean; + onLoadMoreComments?: () => void; } -const MemoCommentSection = ({ memo, comments, parentPage }: Props) => { +const MemoCommentSection = ({ memo, comments, parentPage, hasMoreComments, isFetchingMoreComments, onLoadMoreComments }: Props) => { const t = useTranslate(); const currentUser = useCurrentUser(); const [showEditor, setShowEditor] = useState(false); @@ -71,6 +74,14 @@ const MemoCommentSection = ({ memo, comments, parentPage }: Props) => { ))} + {hasMoreComments && ( +
+ +
+ )} ); diff --git a/web/src/hooks/useMemoQueries.ts b/web/src/hooks/useMemoQueries.ts index e92998671..2a9080c63 100644 --- a/web/src/hooks/useMemoQueries.ts +++ b/web/src/hooks/useMemoQueries.ts @@ -3,6 +3,7 @@ import { FieldMaskSchema } from "@bufbuild/protobuf/wkt"; import type { InfiniteData } from "@tanstack/react-query"; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { memoServiceClient } from "@/connect"; +import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { userKeys } from "@/hooks/useUserQueries"; import type { ListMemosRequest, ListMemosResponse, Memo } from "@/types/proto/api/v1/memo_service_pb"; import { ListMemoCommentsRequestSchema, ListMemosRequestSchema, MemoSchema } from "@/types/proto/api/v1/memo_service_pb"; @@ -284,3 +285,27 @@ export function useMemoComments(name: string, options?: { enabled?: boolean; pag staleTime: 1000 * 60, // 1 minute }); } + +// useInfiniteMemoComments paginates through every comment via nextPageToken, instead of +// stopping at the server's default page size (the cause of comments being truncated to 10). +export function useInfiniteMemoComments(name: string, options?: { enabled?: boolean; pageSize?: number }) { + const pageSize = options?.pageSize ?? DEFAULT_LIST_MEMOS_PAGE_SIZE; + return useInfiniteQuery({ + queryKey: [...memoKeys.comments(name), "infinite", pageSize], + queryFn: async ({ pageParam }) => { + const response = await memoServiceClient.listMemoComments( + create(ListMemoCommentsRequestSchema, { + name, + pageSize, + pageToken: pageParam || "", + }), + ); + return response; + }, + initialPageParam: "", + getNextPageParam: (lastPage) => lastPage.nextPageToken || undefined, + select: (data) => data.pages.flatMap((page) => page.memos), + enabled: options?.enabled ?? true, + staleTime: 1000 * 60, // 1 minute + }); +} diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 05266198c..8eae2b7e0 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -10,7 +10,7 @@ import MobileHeader from "@/components/MobileHeader"; import { memoNamePrefix } from "@/helpers/resource-names"; import useMediaQuery from "@/hooks/useMediaQuery"; import useMemoDetailError from "@/hooks/useMemoDetailError"; -import { useMemo, useMemoComments } from "@/hooks/useMemoQueries"; +import { useInfiniteMemoComments, useMemo } from "@/hooks/useMemoQueries"; import { useSharedMemo, withShareAttachmentLinks } from "@/hooks/useMemoShareQueries"; import { cn } from "@/lib/utils"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; @@ -48,10 +48,14 @@ const MemoDetail = () => { enabled: !!memo?.parent, }); - const { data: commentsResponse } = useMemoComments(memoName, { + const { + data: comments = [], + fetchNextPage: fetchNextComments, + hasNextPage: hasNextComments, + isFetchingNextPage: isFetchingNextComments, + } = useInfiniteMemoComments(memoName, { enabled: !!memo, }); - const comments = commentsResponse?.memos || []; useEffect(() => { if (!hash || comments.length === 0) return; @@ -110,7 +114,14 @@ const MemoDetail = () => { showPinned onShareImageDialogOpenChange={setShareImageDialogOpen} /> - + {md && (