From a3eb518dcccaebd0f48c708c189ad2fcb07f0f73 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Sun, 21 Aug 2022 13:50:18 +0200 Subject: fix(comments): load all comments on a post Previously, only the first 10 comments was loaded. So I update the fetching method to retrieve all the comments on a post. Also, I choose to order comments on client side because of a bug with WPGraphQL. Finally, I renamed the Comment type to SingleComment to avoid conflict with existing types. --- src/components/organisms/layout/comment.tsx | 4 +- .../organisms/layout/comments-list.fixture.tsx | 4 +- src/components/organisms/layout/comments-list.tsx | 12 +- src/pages/article/[slug].tsx | 8 +- src/services/graphql/comments.query.ts | 37 ++++-- src/services/graphql/comments.ts | 144 +++++++++++++++------ src/ts/types/app.ts | 4 +- src/ts/types/graphql/queries.ts | 4 +- src/ts/types/raw-data.ts | 6 + src/utils/hooks/use-comments.tsx | 27 +--- 10 files changed, 159 insertions(+), 91 deletions(-) diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index f62f95c..497a04c 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -1,7 +1,7 @@ import Button from '@components/atoms/buttons/button'; import Link from '@components/atoms/links/link'; import Meta from '@components/molecules/layout/meta'; -import { type Comment as CommentType } from '@ts/types/app'; +import { type SingleComment } from '@ts/types/app'; import useSettings from '@utils/hooks/use-settings'; import Image from 'next/image'; import Script from 'next/script'; @@ -12,7 +12,7 @@ import CommentForm, { type CommentFormProps } from '../forms/comment-form'; import styles from './comment.module.scss'; export type CommentProps = Pick< - CommentType, + SingleComment, 'approved' | 'content' | 'id' | 'meta' | 'parentId' > & Pick & { diff --git a/src/components/organisms/layout/comments-list.fixture.tsx b/src/components/organisms/layout/comments-list.fixture.tsx index 2618f77..f2a1d26 100644 --- a/src/components/organisms/layout/comments-list.fixture.tsx +++ b/src/components/organisms/layout/comments-list.fixture.tsx @@ -1,6 +1,6 @@ -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; -export const comments: Comment[] = [ +export const comments: SingleComment[] = [ { approved: true, content: diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx index 97eccb7..deb0776 100644 --- a/src/components/organisms/layout/comments-list.tsx +++ b/src/components/organisms/layout/comments-list.tsx @@ -1,7 +1,7 @@ -import SingleComment, { +import Comment, { type CommentProps, } from '@components/organisms/layout/comment'; -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; import { FC } from 'react'; import styles from './comments-list.module.scss'; @@ -9,7 +9,7 @@ export type CommentsListProps = Pick & { /** * An array of comments. */ - comments: Comment[]; + comments: SingleComment[]; /** * The maximum depth. Use `0` to not display nested comments. */ @@ -30,18 +30,18 @@ const CommentsList: FC = ({ /** * Get each comment wrapped in a list item. * - * @param {Comment[]} commentsList - An array of comments. + * @param {SingleComment[]} commentsList - An array of comments. * @returns {JSX.Element[]} The list items. */ const getItems = ( - commentsList: Comment[], + commentsList: SingleComment[], startLevel: number ): JSX.Element[] => { const isLastLevel = startLevel === depth; return commentsList.map(({ replies, ...comment }) => (
  • - = async ({ params, }) => { const post = await getArticleBySlug(params!.slug as PostParams['slug']); - const comments = await getPostComments(post.id as number); + const comments = await getAllComments({ contentId: post.id as number }); const translation = await loadTranslation(locale); return { diff --git a/src/services/graphql/comments.query.ts b/src/services/graphql/comments.query.ts index ef93e89..5110db3 100644 --- a/src/services/graphql/comments.query.ts +++ b/src/services/graphql/comments.query.ts @@ -1,21 +1,32 @@ /** * Query the comments data by post id. */ -export const commentsQuery = `query CommentsByPostId($contentId: ID!) { - comments(where: {contentId: $contentId, order: ASC, orderby: COMMENT_DATE}) { - nodes { - approved - author { - node { - gravatarUrl - name - url +export const commentsQuery = `query CommentsByPostId($contentId: ID!, $first: Int = 10, $after: String = "") { + comments( + where: {contentId: $contentId} + first: $first + after: $after + ) { + edges { + cursor + node { + approved + author { + node { + gravatarUrl + name + url + } } + content + databaseId + date + parentDatabaseId } - content - databaseId - date - parentDatabaseId + } + pageInfo { + hasNextPage + endCursor } } }`; diff --git a/src/services/graphql/comments.ts b/src/services/graphql/comments.ts index 86b6a35..41f80b3 100644 --- a/src/services/graphql/comments.ts +++ b/src/services/graphql/comments.ts @@ -1,46 +1,61 @@ -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; import { GraphQLEdgesInput } from '@ts/types/graphql/generics'; import { SendCommentInput, SentComment } from '@ts/types/graphql/mutations'; import { ContentId } from '@ts/types/graphql/queries'; -import { RawComment } from '@ts/types/raw-data'; +import { RawComment, RawCommentsPage } from '@ts/types/raw-data'; import { getAuthorFromRawData } from '@utils/helpers/author'; import { fetchAPI } from './api'; import { sendCommentMutation } from './comments.mutation'; import { commentsQuery } from './comments.query'; -type FetchCommentsInput = ContentId & - Pick; - /** - * Retrieve the comments list from GraphQL. + * Convert a comment from RawComment type to SingleComment type. * - * @param {FetchCommentsInput} variables - An object of variables. - * @returns {Promise} The raw comments. + * @param {RawComment} comment - A raw comment. + * @returns {SingleComment} A formatted comment. */ -export const fetchComments = async ( - variables: FetchCommentsInput -): Promise => { - const response = await fetchAPI({ - query: commentsQuery, - variables, - }); +export const getCommentFromRawData = (comment: RawComment): SingleComment => { + const { author, databaseId, date, parentDatabaseId, ...data } = comment; - return response.comments.nodes; + return { + id: databaseId, + meta: { + author: getAuthorFromRawData(author.node, 'comment'), + date, + }, + parentId: parentDatabaseId === 0 ? undefined : parentDatabaseId, + replies: [], + ...data, + }; +}; + +/** + * Convert an array of RawComment type to an array of SingleComment type. + * + * @param {RawComment[]} comments - The raw comments. + * @returns {SingleComment[]} The formatted comments. + */ +export const getCommentsFromRawData = ( + comments: RawComment[] +): SingleComment[] => { + return comments.map((comment) => getCommentFromRawData(comment)); }; /** * Create a comments tree with replies. * - * @param {Comment[]} comments - A flatten comments list. - * @returns {Comment[]} An array of comments with replies. + * @param {SingleComment[]} comments - A flatten comments list. + * @returns {SingleComment[]} An array of comments with replies. */ -export const buildCommentsTree = (comments: Comment[]): Comment[] => { +export const buildCommentsTree = ( + comments: SingleComment[] +): SingleComment[] => { type CommentsHashTable = { - [key: string]: Comment; + [key: string]: SingleComment; }; const hashTable: CommentsHashTable = Object.create(null); - const commentsTree: Comment[] = []; + const commentsTree: SingleComment[] = []; comments.forEach( (comment) => (hashTable[comment.id] = { ...comment, replies: [] }) @@ -57,36 +72,85 @@ export const buildCommentsTree = (comments: Comment[]): Comment[] => { return commentsTree; }; +type FetchCommentsInput = ContentId & + Pick; + /** - * Convert a comment from RawComment to Comment type. + * Retrieve a raw comments page from GraphQL. * - * @param {RawComment} comment - A raw comment. - * @returns {Comment} A formatted comment. + * @param {FetchCommentsInput} variables - An object of variables. + * @returns {Promise} A raw comments page. */ -export const getCommentFromRawData = (comment: RawComment): Comment => { - const { author, databaseId, date, parentDatabaseId, ...data } = comment; +export const fetchRawComments = async ( + variables: FetchCommentsInput +): Promise => { + const response = await fetchAPI({ + query: commentsQuery, + variables, + }); return { - id: databaseId, - meta: { - author: getAuthorFromRawData(author.node, 'comment'), - date, - }, - parentId: parentDatabaseId, - replies: [], - ...data, + comments: response.comments.edges.map((edge) => edge.node), + hasNextPage: response.comments.pageInfo.hasNextPage, + endCursor: response.comments.pageInfo.endCursor, }; }; /** - * Retrieve a comments list by post id. + * Fetch recursively all the comments on a post. + * + * @param {FetchCommentsInput} variables - An object of query variables. + * @param {RawCommentsPage[]} pages - An accumulator to keep track of pages. + * @returns {Promise} The raw comments pages. + */ +export const fetchAllRawCommentsPages = async ( + variables: FetchCommentsInput, + pages: RawCommentsPage[] = [] +): Promise => { + return fetchRawComments(variables).then((page) => { + pages.push(page); + + if (page.hasNextPage) { + return fetchAllRawCommentsPages( + { ...variables, after: page.endCursor }, + pages + ); + } else { + return pages; + } + }); +}; + +/** + * Method to compare two comments dates and sort them from older to newest. + * + * @param {SingleComment} a - A comment. + * @param {SingleComment} b - Another comment. + * @returns {number} The difference between dates. + */ +export const compareCommentsDate = ( + a: SingleComment, + b: SingleComment +): number => { + return +new Date(a.meta.date) - +new Date(b.meta.date); +}; + +/** + * Retrieve all the comments on a post. * * @param {number} id - A post id. - * @returns {Promise} The comments list. + * @returns {Promise} The comments list. */ -export const getPostComments = async (id: number): Promise => { - const rawComments = await fetchComments({ contentId: id }); - const comments = rawComments.map((comment) => getCommentFromRawData(comment)); +export const getAllComments = async ({ + contentId, +}: { + contentId: number; +}): Promise => { + const pages = await fetchAllRawCommentsPages({ contentId }); + const comments = pages + .map((page) => getCommentsFromRawData(page.comments)) + .flat() + .sort(compareCommentsDate); return buildCommentsTree(comments); }; @@ -95,7 +159,7 @@ export const getPostComments = async (id: number): Promise => { * Send a comment using GraphQL API. * * @param {SendCommentVars} data - The comment data. - * @returns {Promise} The mutation response. + * @returns {Promise} The mutation response. */ export const sendComment = async ( data: SendCommentInput diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts index 7bf1541..c11c31b 100644 --- a/src/ts/types/app.ts +++ b/src/ts/types/app.ts @@ -39,13 +39,13 @@ export type CommentMeta = { date: string; }; -export type Comment = { +export type SingleComment = { approved: boolean; content: string; id: number; meta: CommentMeta; parentId?: number; - replies: Comment[]; + replies: SingleComment[]; }; export type Dates = { diff --git a/src/ts/types/graphql/queries.ts b/src/ts/types/graphql/queries.ts index cc7b62b..c29eeb3 100644 --- a/src/ts/types/graphql/queries.ts +++ b/src/ts/types/graphql/queries.ts @@ -96,7 +96,7 @@ export type QueriesResponseMap = { [articlesEndCursorQuery]: ArticlesResponse; [articlesQuery]: ArticlesResponse>; [articlesSlugQuery]: ArticlesResponse>; - [commentsQuery]: CommentsResponse>; + [commentsQuery]: CommentsResponse>; [thematicBySlugQuery]: ThematicResponse; [thematicsListQuery]: ThematicsResponse>; [thematicsSlugQuery]: ThematicsResponse>; @@ -128,7 +128,7 @@ export type QueriesInputMap = { [articlesEndCursorQuery]: QueryEdges & Search; [articlesQuery]: QueryEdges & Search; [articlesSlugQuery]: QueryEdges & Search; - [commentsQuery]: ContentId; + [commentsQuery]: ContentId & QueryEdges; [thematicBySlugQuery]: Slug; [thematicsListQuery]: QueryEdges & Search; [thematicsSlugQuery]: QueryEdges & Search; diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts index ae7f7c6..022016e 100644 --- a/src/ts/types/raw-data.ts +++ b/src/ts/types/raw-data.ts @@ -44,6 +44,12 @@ export type RawComment = { parentDatabaseId: number; }; +export type RawCommentsPage = { + comments: RawComment[]; + hasNextPage: boolean; + endCursor: string; +}; + export type RawCover = { altText: string; mediaDetails: { diff --git a/src/utils/hooks/use-comments.tsx b/src/utils/hooks/use-comments.tsx index cb0848b..a695bd7 100644 --- a/src/utils/hooks/use-comments.tsx +++ b/src/utils/hooks/use-comments.tsx @@ -1,38 +1,25 @@ -import { fetchAPI } from '@services/graphql/api'; -import { - buildCommentsTree, - getCommentFromRawData, -} from '@services/graphql/comments'; -import { commentsQuery } from '@services/graphql/comments.query'; -import { Comment } from '@ts/types/app'; -import { RawComment } from '@ts/types/raw-data'; +import { getAllComments } from '@services/graphql/comments'; +import { SingleComment } from '@ts/types/app'; import useSWR from 'swr'; export type UseCommentsConfig = { contentId?: string | number; - fallback?: Comment[]; + fallback?: SingleComment[]; }; /** * Retrieve the comments of a page/article. * * @param {string | number} contentId - A page/article id. - * @returns {Comment[]|undefined} + * @returns {SingleComment[]|undefined} */ const useComments = ({ contentId, fallback, -}: UseCommentsConfig): Comment[] | undefined => { - const { data } = useSWR( - contentId ? { query: commentsQuery, variables: { contentId } } : null, - fetchAPI - ); +}: UseCommentsConfig): SingleComment[] | undefined => { + const { data } = useSWR(contentId ? { contentId } : null, getAllComments); - const comments = data?.comments.nodes.map((comment) => - getCommentFromRawData(comment) - ); - - return comments ? buildCommentsTree(comments) : fallback; + return data || fallback; }; export default useComments; -- cgit v1.2.3