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 && (