179 lines
5.2 KiB
JavaScript
179 lines
5.2 KiB
JavaScript
import React, { useEffect, useMemo } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useParams } from 'react-router';
|
|
|
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
import { Button, Spinner } from '@edx/paragon';
|
|
|
|
import { EndorsementStatus, ThreadType } from '../../data/constants';
|
|
import { useDispatchWithState } from '../../data/hooks';
|
|
import { Post } from '../posts';
|
|
import { selectThread } from '../posts/data/selectors';
|
|
import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
|
|
import { filterPosts } from '../utils';
|
|
import { selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages } from './data/selectors';
|
|
import { fetchThreadComments } from './data/thunks';
|
|
import { Comment, ResponseEditor } from './comment';
|
|
import messages from './messages';
|
|
|
|
function usePost(postId) {
|
|
const dispatch = useDispatch();
|
|
const thread = useSelector(selectThread(postId));
|
|
|
|
useEffect(() => {
|
|
if (thread && !thread.read) {
|
|
dispatch(markThreadAsRead(postId));
|
|
}
|
|
}, [postId]);
|
|
|
|
return thread;
|
|
}
|
|
|
|
function usePostComments(postId, endorsed = null) {
|
|
const [isLoading, dispatch] = useDispatchWithState();
|
|
const comments = useSelector(selectThreadComments(postId, endorsed));
|
|
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
|
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
|
const handleLoadMoreResponses = async () => dispatch(fetchThreadComments(postId, {
|
|
endorsed,
|
|
page: currentPage + 1,
|
|
}));
|
|
useEffect(() => {
|
|
dispatch(fetchThreadComments(postId, {
|
|
endorsed,
|
|
page: 1,
|
|
}));
|
|
}, [postId]);
|
|
return {
|
|
comments,
|
|
hasMorePages,
|
|
isLoading,
|
|
handleLoadMoreResponses,
|
|
};
|
|
}
|
|
|
|
function DiscussionCommentsView({
|
|
postType,
|
|
postId,
|
|
intl,
|
|
endorsed,
|
|
isClosed,
|
|
}) {
|
|
const {
|
|
comments,
|
|
hasMorePages,
|
|
isLoading,
|
|
handleLoadMoreResponses,
|
|
} = usePostComments(postId, endorsed);
|
|
const sortedComments = useMemo(() => [...filterPosts(comments, 'endorsed'),
|
|
...filterPosts(comments, 'unendorsed')], [comments]);
|
|
|
|
return (
|
|
<>
|
|
{((hasMorePages && isLoading) || !isLoading)
|
|
&& (
|
|
<div className="mx-4 text-primary-700" role="heading" aria-level="2" style={{ lineHeight: '28px' }}>
|
|
{endorsed === EndorsementStatus.ENDORSED
|
|
? intl.formatMessage(messages.endorsedResponseCount, { num: sortedComments.length })
|
|
: intl.formatMessage(messages.responseCount, { num: sortedComments.length })}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mx-4" role="list">
|
|
{sortedComments.map(comment => (
|
|
<Comment comment={comment} key={comment.id} postType={postType} isClosedPost={isClosed} />
|
|
))}
|
|
{hasMorePages && !isLoading && (
|
|
<Button
|
|
onClick={handleLoadMoreResponses}
|
|
variant="link"
|
|
block="true"
|
|
className="card p-4 mb-4 font-weight-500 font-size-14"
|
|
style={{
|
|
lineHeight: '20px',
|
|
}}
|
|
data-testid="load-more-comments"
|
|
>
|
|
{intl.formatMessage(messages.loadMoreResponses)}
|
|
</Button>
|
|
)}
|
|
{isLoading
|
|
&& (
|
|
<div className="card my-4 p-4 d-flex align-items-center">
|
|
<Spinner animation="border" variant="primary" />
|
|
</div>
|
|
)}
|
|
{!!sortedComments.length && !isClosed
|
|
&& <ResponseEditor postId={postId} addWrappingDiv />}
|
|
</div>
|
|
</>
|
|
|
|
);
|
|
}
|
|
|
|
DiscussionCommentsView.propTypes = {
|
|
postId: PropTypes.string.isRequired,
|
|
postType: PropTypes.string.isRequired,
|
|
isClosed: PropTypes.bool.isRequired,
|
|
intl: intlShape.isRequired,
|
|
endorsed: PropTypes.oneOf([
|
|
EndorsementStatus.ENDORSED, EndorsementStatus.UNENDORSED, EndorsementStatus.DISCUSSION,
|
|
]).isRequired,
|
|
};
|
|
|
|
function CommentsView({ intl }) {
|
|
const { postId } = useParams();
|
|
const thread = usePost(postId);
|
|
const dispatch = useDispatch();
|
|
if (!thread) {
|
|
dispatch(fetchThread(postId, true));
|
|
return (
|
|
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<div className="discussion-comments d-flex flex-column m-4 p-4.5 card">
|
|
<Post post={thread} />
|
|
{!thread.closed && <ResponseEditor postId={postId} /> }
|
|
</div>
|
|
{thread.type === ThreadType.DISCUSSION
|
|
&& (
|
|
<DiscussionCommentsView
|
|
postId={postId}
|
|
intl={intl}
|
|
postType={thread.type}
|
|
endorsed={EndorsementStatus.DISCUSSION}
|
|
isClosed={thread.closed}
|
|
/>
|
|
)}
|
|
{thread.type === ThreadType.QUESTION && (
|
|
<>
|
|
<DiscussionCommentsView
|
|
postId={postId}
|
|
intl={intl}
|
|
postType={thread.type}
|
|
endorsed={EndorsementStatus.ENDORSED}
|
|
isClosed={thread.closed}
|
|
/>
|
|
<DiscussionCommentsView
|
|
postId={postId}
|
|
intl={intl}
|
|
postType={thread.type}
|
|
endorsed={EndorsementStatus.UNENDORSED}
|
|
isClosed={thread.closed}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
CommentsView.propTypes = {
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
export default injectIntl(CommentsView);
|