diff --git a/app/api/versions/[versionId]/comments/route.ts b/app/api/versions/[versionId]/comments/route.ts index 446023f..7687e85 100644 --- a/app/api/versions/[versionId]/comments/route.ts +++ b/app/api/versions/[versionId]/comments/route.ts @@ -83,15 +83,23 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { searchParams } = new URL(request.url); const includeResolved = searchParams.get('includeResolved') !== 'false'; + const limit = Math.min(Math.max(1, parseInt(searchParams.get('limit') ?? '200', 10)), 500); + const offset = Math.max(0, parseInt(searchParams.get('offset') ?? '0', 10)); - const commentsRevision = await db.comment.aggregate({ - where: { - versionId, - ...(includeResolved ? {} : { isResolved: false }), - }, - _count: { id: true }, - _max: { updatedAt: true }, - }); + const commentsFilter = { + versionId, + parentId: null as null, + ...(includeResolved ? {} : { isResolved: false }), + }; + + const [commentsRevision, total] = await Promise.all([ + db.comment.aggregate({ + where: { versionId, ...(includeResolved ? {} : { isResolved: false }) }, + _count: { id: true }, + _max: { updatedAt: true }, + }), + db.comment.count({ where: commentsFilter }), + ]); const etag = `"comments:${versionId}:${includeResolved ? 1 : 0}:${commentsRevision._count.id}:${commentsRevision._max.updatedAt?.getTime() ?? 0}"`; const ifNoneMatch = request.headers.get('if-none-match'); @@ -109,12 +117,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { } const comments = await db.comment.findMany({ - where: { - versionId, - parentId: null, // Only top-level comments - ...(includeResolved ? {} : { isResolved: false }), - }, + where: commentsFilter, orderBy: { timestamp: 'asc' }, + skip: offset, + take: limit, select: { id: true, content: true, @@ -162,7 +168,13 @@ export async function GET(request: NextRequest, { params }: RouteParams) { }, }); - const response = successResponse({ comments }); + const response = successResponse({ + comments, + total, + hasMore: offset + comments.length < total, + offset, + limit, + }); response.headers.set('ETag', etag); return withCacheControl(response, 'private, no-cache'); } catch (error) { diff --git a/components/video-page/hooks/use-video-page-data.ts b/components/video-page/hooks/use-video-page-data.ts index e8b8a85..dda2440 100644 --- a/components/video-page/hooks/use-video-page-data.ts +++ b/components/video-page/hooks/use-video-page-data.ts @@ -38,20 +38,38 @@ export function useVideoPageData({ if (etag) headers['If-None-Match'] = etag; } - const res = await fetch(`/api/versions/${versionId}/comments?includeResolved=true`, { - cache: 'no-store', - headers, - }); + const LIMIT = 200; + let offset = 0; + let allComments: Comment[] = []; + let latestEtag: string | null = null; - if (res.status === 304) return; - if (!res.ok) return; + // Fetch pages until we have all comments + while (true) { + const res = await fetch( + `/api/versions/${versionId}/comments?includeResolved=true&limit=${LIMIT}&offset=${offset}`, + { cache: 'no-store', headers: offset === 0 ? headers : {} } + ); - const etag = res.headers.get('etag'); - if (etag) commentsEtagRef.current.set(versionId, etag); + if (offset === 0 && res.status === 304) return; + if (!res.ok) return; - const payload = await res.json(); - const commentsList = payload?.data?.comments; - if (!Array.isArray(commentsList)) return; + if (offset === 0) { + latestEtag = res.headers.get('etag'); + } + + const payload = await res.json(); + const page: Comment[] = payload?.data?.comments; + if (!Array.isArray(page)) return; + + allComments = allComments.concat(page); + + if (!payload?.data?.hasMore) break; + offset += LIMIT; + } + + if (latestEtag) commentsEtagRef.current.set(versionId, latestEtag); + + const commentsList = allComments; setVideo((prev) => { if (!prev) return prev;