diff --git a/src/components/boris/BorisComments/index.tsx b/src/components/boris/BorisComments/index.tsx index b4350c44..4a61c8d0 100644 --- a/src/components/boris/BorisComments/index.tsx +++ b/src/components/boris/BorisComments/index.tsx @@ -8,16 +8,27 @@ import { Footer } from '~/components/main/Footer'; import { Card } from '~/components/containers/Card'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { selectAuthUser } from '~/redux/auth/selectors'; -import { IComment, INode } from '~/redux/types'; +import { IComment, IFile, INode } from '~/redux/types'; interface IProps { isLoadingComments: boolean; commentCount: number; node: INode; comments: IComment[]; + onDelete: (id: IComment['id'], locked: boolean) => void; + onLoadMoreComments: () => void; + onShowPhotoswipe: (images: IFile[], index: number) => void; } -const BorisComments: FC<IProps> = ({ isLoadingComments, node, commentCount, comments }) => { +const BorisComments: FC<IProps> = ({ + node, + commentCount, + comments, + isLoadingComments, + onLoadMoreComments, + onDelete, + onShowPhotoswipe, +}) => { const user = useShallowSelect(selectAuthUser); return ( @@ -28,7 +39,15 @@ const BorisComments: FC<IProps> = ({ isLoadingComments, node, commentCount, comm {isLoadingComments ? ( <NodeNoComments is_loading count={7} /> ) : ( - <NodeComments comments={comments} count={commentCount} user={user} order="ASC" /> + <NodeComments + comments={comments} + count={commentCount} + user={user} + order="ASC" + onLoadMoreComments={onLoadMoreComments} + onDelete={onDelete} + onShowPhotoswipe={onShowPhotoswipe} + /> )} </Group> diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index 6ee92c31..941c5d40 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -1,6 +1,6 @@ import React, { FC, HTMLAttributes, memo } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { IComment, ICommentGroup } from '~/redux/types'; +import { IComment, ICommentGroup, IFile } from '~/redux/types'; import { CommentContent } from '~/components/comment/CommentContent'; import styles from './styles.module.scss'; import { CommendDeleted } from '../../node/CommendDeleted'; @@ -13,7 +13,7 @@ type IProps = HTMLAttributes<HTMLDivElement> & { is_same?: boolean; can_edit?: boolean; onDelete: (id: IComment['id'], isLocked: boolean) => void; - modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; + modalShowPhotoswipe: (images: IFile[], index: number) => void; }; const Comment: FC<IProps> = memo( diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index cccdb985..8785f450 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -21,7 +21,7 @@ interface IProps { comment: IComment; can_edit: boolean; onDelete: (id: IComment['id'], isLocked: boolean) => void; - modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; + modalShowPhotoswipe: (images: IFile[], index: number) => void; } const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalShowPhotoswipe }) => { diff --git a/src/components/node/NodeBottomBlock/index.tsx b/src/components/node/NodeBottomBlock/index.tsx index 087b5829..7e3c6b78 100644 --- a/src/components/node/NodeBottomBlock/index.tsx +++ b/src/components/node/NodeBottomBlock/index.tsx @@ -6,7 +6,7 @@ import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock'; import { NodeCommentForm } from '~/components/node/NodeCommentForm'; import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock'; import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; -import { IComment, INode } from '~/redux/types'; +import { IComment, IFile, INode } from '~/redux/types'; import { useUser } from '~/utils/hooks/user/userUser'; import { NodeTagsBlock } from '~/components/node/NodeTagsBlock'; import { INodeRelated } from '~/redux/node/types'; @@ -21,6 +21,9 @@ interface IProps { commentsCount: number; isLoadingComments: boolean; related: INodeRelated; + onDeleteComment: (id: IComment['id'], locked: boolean) => void; + onLoadMoreComments: () => void; + onShowPhotoswipe: (images: IFile[], index: number) => void; } const NodeBottomBlock: FC<IProps> = ({ @@ -31,6 +34,9 @@ const NodeBottomBlock: FC<IProps> = ({ commentsCount, commentsOrder, related, + onDeleteComment, + onLoadMoreComments, + onShowPhotoswipe, }) => { const { inline } = useNodeBlocks(node, isLoading); const { is_user } = useUser(); @@ -53,6 +59,9 @@ const NodeBottomBlock: FC<IProps> = ({ count={commentsCount} order={commentsOrder} node={node} + onDelete={onDeleteComment} + onLoadMoreComments={onLoadMoreComments} + onShowPhotoswipe={onShowPhotoswipe} /> {is_user && !isLoading && <NodeCommentForm nodeId={node?.id} />} diff --git a/src/components/node/NodeComments/index.tsx b/src/components/node/NodeComments/index.tsx index 075c68a8..04cb1310 100644 --- a/src/components/node/NodeComments/index.tsx +++ b/src/components/node/NodeComments/index.tsx @@ -18,56 +18,58 @@ interface IProps { count: INodeState['comment_count']; user: IUser; order?: 'ASC' | 'DESC'; + onDelete: (id: IComment['id'], locked: boolean) => void; + onLoadMoreComments: () => void; + onShowPhotoswipe: (images: IFile[], index: number) => void; } -const NodeComments: FC<IProps> = memo(({ comments, user, count = 0, order = 'DESC' }) => { - const dispatch = useDispatch(); - const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]); +const NodeComments: FC<IProps> = memo( + ({ + onLoadMoreComments, + onDelete, + onShowPhotoswipe, + comments, + user, + count = 0, + order = 'DESC', + }) => { + const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]); - const groupped: ICommentGroup[] = useMemo( - () => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []), - [comments, order] - ); + const groupped: ICommentGroup[] = useMemo( + () => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []), + [comments, order] + ); - const onDelete = useCallback( - (id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)), - [dispatch] - ); - const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]); - const onShowPhotoswipe = useCallback( - (images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)), - [dispatch] - ); + const more = useMemo( + () => + left > 0 && ( + <div className={styles.more} onClick={onLoadMoreComments}> + Показать ещё{' '} + {plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')} + {left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''} + </div> + ), + [left, onLoadMoreComments] + ); - const more = useMemo( - () => - left > 0 && ( - <div className={styles.more} onClick={onLoadMoreComments}> - Показать ещё{' '} - {plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')} - {left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''} - </div> - ), - [left, onLoadMoreComments] - ); + return ( + <div className={styles.wrap}> + {order === 'DESC' && more} - return ( - <div className={styles.wrap}> - {order === 'DESC' && more} + {groupped.map(group => ( + <Comment + key={group.ids.join()} + comment_group={group} + can_edit={canEditComment(group, user)} + onDelete={onDelete} + modalShowPhotoswipe={onShowPhotoswipe} + /> + ))} - {groupped.map(group => ( - <Comment - key={group.ids.join()} - comment_group={group} - can_edit={canEditComment(group, user)} - onDelete={onDelete} - modalShowPhotoswipe={onShowPhotoswipe} - /> - ))} - - {order === 'ASC' && more} - </div> - ); -}); + {order === 'ASC' && more} + </div> + ); + } +); export { NodeComments }; diff --git a/src/components/node/NodeCommentsBlock/index.tsx b/src/components/node/NodeCommentsBlock/index.tsx index aeab8eb2..f873eb05 100644 --- a/src/components/node/NodeCommentsBlock/index.tsx +++ b/src/components/node/NodeCommentsBlock/index.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeComments } from '~/components/node/NodeComments'; -import { IComment, INode } from '~/redux/types'; +import { IComment, IFile, INode } from '~/redux/types'; import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; import { useUser } from '~/utils/hooks/user/userUser'; @@ -12,16 +12,36 @@ interface IProps { count: number; isLoading: boolean; isLoadingComments: boolean; + onDelete: (id: IComment['id'], locked: boolean) => void; + onLoadMoreComments: () => void; + onShowPhotoswipe: (images: IFile[], index: number) => void; } -const NodeCommentsBlock: FC<IProps> = ({ isLoading, isLoadingComments, node, comments, count }) => { +const NodeCommentsBlock: FC<IProps> = ({ + onLoadMoreComments, + onDelete, + onShowPhotoswipe, + isLoading, + isLoadingComments, + node, + comments, + count, +}) => { const user = useUser(); const { inline } = useNodeBlocks(node, isLoading); return isLoading || isLoadingComments || (!comments.length && !inline) ? ( <NodeNoComments is_loading={isLoadingComments || isLoading} /> ) : ( - <NodeComments count={count} comments={comments} user={user} order="DESC" /> + <NodeComments + count={count} + comments={comments} + user={user} + order="DESC" + onLoadMoreComments={onLoadMoreComments} + onDelete={onDelete} + onShowPhotoswipe={onShowPhotoswipe} + /> ); }; diff --git a/src/constants/api.ts b/src/constants/api.ts index f834633c..becc08f1 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,5 +1,6 @@ import { IComment, INode } from '~/redux/types'; import { ISocialProvider } from '~/redux/auth/types'; +import { COMMENTS_DISPLAY } from '~/redux/node/constants'; export const API = { BASE: process.env.REACT_APP_API_HOST, @@ -28,6 +29,8 @@ export const API = { GET_NODE: (id: number | string) => `/node/${id}`, COMMENT: (id: INode['id']) => `/node/${id}/comment`, + COMMENT_INFINITE: (id: INode['id'], skip: number) => + `/node/${id}/comment?take=${COMMENTS_DISPLAY}&skip=${skip}`, RELATED: (id: INode['id']) => `/node/${id}/related`, UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`, POST_LIKE: (id: INode['id']) => `/node/${id}/like`, diff --git a/src/layouts/BorisLayout/index.tsx b/src/layouts/BorisLayout/index.tsx index e5f79cfe..5816cc3f 100644 --- a/src/layouts/BorisLayout/index.tsx +++ b/src/layouts/BorisLayout/index.tsx @@ -115,6 +115,9 @@ const BorisLayout: FC<IProps> = () => { commentCount={node.comment_count} node={node.current} comments={node.comments} + onDelete={console.log} + onLoadMoreComments={console.log} + onShowPhotoswipe={console.log} /> </Switch> } diff --git a/src/layouts/NodeLayout/index.tsx b/src/layouts/NodeLayout/index.tsx index f14cb3f9..31426a76 100644 --- a/src/layouts/NodeLayout/index.tsx +++ b/src/layouts/NodeLayout/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from 'react'; +import React, { FC, memo, useCallback } from 'react'; import { Route, RouteComponentProps } from 'react-router'; import { selectNode } from '~/redux/node/selectors'; import { Card } from '~/components/containers/Card'; @@ -20,6 +20,10 @@ import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen'; import styles from './styles.module.scss'; import { useNodeFetcher } from '~/utils/hooks/node/useNodeFetcher'; +import { useNodeComments } from '~/utils/hooks/node/useNodeComments'; +import { IFile } from '~/redux/types'; +import { modalShowPhotoswipe } from '~/redux/modal/actions'; +import { useDispatch } from 'react-redux'; type IProps = RouteComponentProps<{ id: string }> & {}; @@ -30,8 +34,16 @@ const NodeLayout: FC<IProps> = memo( }, }) => { const { node, isLoading } = useNodeFetcher(parseInt(id, 10)); + const { + comments, + isLoading: isLoadingComments, + count: commentsCount, + onDelete, + onLoadMoreComments, + onShowPhotoswipe, + } = useNodeComments(parseInt(id, 10)); - const { comments, comment_count, is_loading_comments, related } = useShallowSelect(selectNode); + const { related } = useShallowSelect(selectNode); useNodeCoverImage(node); useScrollToTop([id]); @@ -52,12 +64,15 @@ const NodeLayout: FC<IProps> = memo( <NodeBottomBlock node={node} - isLoadingComments={is_loading_comments} comments={comments} isLoading={isLoading} - commentsCount={comment_count} + isLoadingComments={isLoadingComments} + commentsCount={commentsCount} commentsOrder="DESC" related={related} + onShowPhotoswipe={onShowPhotoswipe} + onDeleteComment={onDelete} + onLoadMoreComments={onLoadMoreComments} /> <Footer /> diff --git a/src/utils/hooks/node/useNodeComments.ts b/src/utils/hooks/node/useNodeComments.ts new file mode 100644 index 00000000..d108ed4f --- /dev/null +++ b/src/utils/hooks/node/useNodeComments.ts @@ -0,0 +1,54 @@ +import { IComment, IFile, INode } from '~/redux/types'; +import useSWRInfinite from 'swr/infinite'; +import { ApiGetNodeCommentsResponse } from '~/redux/node/api'; +import { api, cleanResult } from '~/utils/api'; +import { API } from '~/constants/api'; +import { useCallback, useMemo } from 'react'; +import { COMMENTS_DISPLAY } from '~/redux/node/constants'; +import { nodeLockComment } from '~/redux/node/actions'; +import { useDispatch } from 'react-redux'; +import { modalShowPhotoswipe } from '~/redux/modal/actions'; + +export const fetcher = (url: string) => api.get<ApiGetNodeCommentsResponse>(url).then(cleanResult); + +export const useNodeComments = (id: INode['id']) => { + const dispatch = useDispatch(); + + const getKey = useCallback( + (pageIndex, previousPageData) => { + if (previousPageData && !previousPageData?.comments?.length) return null; + return API.NODE.COMMENT_INFINITE(id, pageIndex * COMMENTS_DISPLAY); + }, + [id] + ); + + const { data, error, isValidating, size, setSize } = useSWRInfinite(getKey, fetcher); + + const comments = useMemo<IComment[]>( + () => (data || []).reduce((acc, { comments }) => [...acc, ...comments], [] as IComment[]), + [data] + ); + + const count = useMemo<number>(() => { + if (!data) { + return 0; + } + + return data[data.length - 1].comment_count || 0; + }, [data]); + + const isLoading = !data && !isValidating; + + const onDelete = useCallback( + (id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)), + [dispatch] + ); + const onLoadMoreComments = useCallback(() => setSize(size + 1), [size, setSize]); + + const onShowPhotoswipe = useCallback( + (images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)), + [dispatch] + ); + + return { comments, count, error, isLoading, onDelete, onLoadMoreComments, onShowPhotoswipe }; +}; diff --git a/src/utils/hooks/node/useNodeFetcher.ts b/src/utils/hooks/node/useNodeFetcher.ts index 7376d8cf..5c727829 100644 --- a/src/utils/hooks/node/useNodeFetcher.ts +++ b/src/utils/hooks/node/useNodeFetcher.ts @@ -5,7 +5,7 @@ import { apiGetNode } from '~/redux/node/api'; export const useNodeFetcher = (id: INode['id']) => { const { data, error, isValidating } = useSWR(`${id}`, apiGetNode); const node = data?.node; - const isLoading = !node && !isValidating; + const isLoading = !data && !isValidating; return { node, error, isLoading }; };