99ab165a8f
Clearer display of takeover amounts Repost from empty search result, from top page, or from claim review changes final touches bump empty comment copy they emptier validation cleanup extra
252 lines
8.1 KiB
JavaScript
252 lines
8.1 KiB
JavaScript
// @flow
|
|
import * as REACTION_TYPES from 'constants/reactions';
|
|
import * as ICONS from 'constants/icons';
|
|
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
|
import React, { useEffect } from 'react';
|
|
import classnames from 'classnames';
|
|
import CommentView from 'component/comment';
|
|
import Spinner from 'component/spinner';
|
|
import Button from 'component/button';
|
|
import Card from 'component/common/card';
|
|
import CommentCreate from 'component/commentCreate';
|
|
import usePersistedState from 'effects/use-persisted-state';
|
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
|
import { sortComments } from 'util/comments';
|
|
import Empty from 'component/common/empty';
|
|
|
|
type Props = {
|
|
comments: Array<Comment>,
|
|
fetchComments: string => void,
|
|
fetchReacts: string => Promise<any>,
|
|
uri: string,
|
|
claimIsMine: boolean,
|
|
myChannels: ?Array<ChannelClaim>,
|
|
isFetchingComments: boolean,
|
|
linkedComment: any,
|
|
totalComments: number,
|
|
fetchingChannels: boolean,
|
|
reactionsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
|
activeChannel: string,
|
|
};
|
|
|
|
function CommentList(props: Props) {
|
|
const {
|
|
fetchComments,
|
|
fetchReacts,
|
|
uri,
|
|
comments,
|
|
claimIsMine,
|
|
myChannels,
|
|
isFetchingComments,
|
|
linkedComment,
|
|
totalComments,
|
|
fetchingChannels,
|
|
reactionsById,
|
|
activeChannel,
|
|
} = props;
|
|
const commentRef = React.useRef();
|
|
const spinnerRef = React.useRef();
|
|
const [sort, setSort] = usePersistedState(
|
|
'comment-sort',
|
|
ENABLE_COMMENT_REACTIONS ? SORT_COMMENTS_BEST : SORT_COMMENTS_NEW
|
|
);
|
|
|
|
const [start] = React.useState(0);
|
|
const [end, setEnd] = React.useState(9);
|
|
// Display comments immediately if not fetching reactions
|
|
// If not, wait to show comments until reactions are fetched
|
|
const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(
|
|
Boolean(reactionsById) || !ENABLE_COMMENT_REACTIONS
|
|
);
|
|
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
|
const hasNoComments = !totalComments;
|
|
const moreBelow = totalComments - end > 0;
|
|
const isMyComment = (channelId: string): boolean => {
|
|
if (myChannels != null && channelId != null) {
|
|
for (let i = 0; i < myChannels.length; i++) {
|
|
if (myChannels[i].claim_id === channelId) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const handleMoreBelow = React.useCallback(() => {
|
|
if (moreBelow) {
|
|
setEnd(end + 10);
|
|
}
|
|
}, [end, setEnd, moreBelow]);
|
|
|
|
useEffect(() => {
|
|
fetchComments(uri);
|
|
}, [fetchComments, uri]);
|
|
|
|
useEffect(() => {
|
|
if (totalComments && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
|
fetchReacts(uri)
|
|
.then(() => {
|
|
setReadyToDisplayComments(true);
|
|
})
|
|
.catch(() => setReadyToDisplayComments(true));
|
|
}
|
|
}, [fetchReacts, uri, totalComments, activeChannel, fetchingChannels]);
|
|
|
|
useEffect(() => {
|
|
if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) {
|
|
commentRef.current.scrollIntoView({ block: 'start' });
|
|
window.scrollBy(0, -100);
|
|
}
|
|
}, [readyToDisplayComments, linkedCommentId]);
|
|
|
|
useEffect(() => {
|
|
function handleCommentScroll(e) {
|
|
// $FlowFixMe
|
|
const rect = spinnerRef.current.getBoundingClientRect();
|
|
|
|
const isInViewport =
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
// $FlowFixMe
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
// $FlowFixMe
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
|
|
if (isInViewport) {
|
|
handleMoreBelow();
|
|
}
|
|
}
|
|
|
|
if (!isFetchingComments && readyToDisplayComments && moreBelow && spinnerRef && spinnerRef.current) {
|
|
window.addEventListener('scroll', handleCommentScroll);
|
|
}
|
|
|
|
return () => window.removeEventListener('scroll', handleCommentScroll);
|
|
}, [moreBelow, handleMoreBelow, spinnerRef, isFetchingComments, readyToDisplayComments]);
|
|
|
|
function prepareComments(arrayOfComments, linkedComment, isFetchingComments) {
|
|
let orderedComments = [];
|
|
|
|
if (linkedComment) {
|
|
if (!linkedComment.parent_id) {
|
|
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.comment_id);
|
|
orderedComments.unshift(linkedComment);
|
|
} else {
|
|
const parentComment = arrayOfComments.find(c => c.comment_id === linkedComment.parent_id);
|
|
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.parent_id);
|
|
|
|
if (parentComment) {
|
|
orderedComments.unshift(parentComment);
|
|
}
|
|
}
|
|
} else {
|
|
orderedComments = arrayOfComments;
|
|
}
|
|
return orderedComments;
|
|
}
|
|
|
|
// Default to newest first for apps that don't have comment reactions
|
|
const sortedComments = reactionsById ? sortComments({ comments, reactionsById, sort, isMyComment }) : [];
|
|
const displayedComments = readyToDisplayComments
|
|
? prepareComments(sortedComments, linkedComment).slice(start, end)
|
|
: [];
|
|
|
|
return (
|
|
<Card
|
|
title={
|
|
totalComments > 0
|
|
? totalComments === 1
|
|
? __('1 comment')
|
|
: __('%total_comments% comments', { total_comments: totalComments })
|
|
: __('Leave a comment')
|
|
}
|
|
titleActions={
|
|
<>
|
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
|
<span className="comment__sort">
|
|
<Button
|
|
button="alt"
|
|
label={__('Best')}
|
|
icon={ICONS.BEST}
|
|
iconSize={18}
|
|
onClick={() => setSort(SORT_COMMENTS_BEST)}
|
|
className={classnames(`button-toggle`, {
|
|
'button-toggle--active': sort === SORT_COMMENTS_BEST,
|
|
})}
|
|
/>
|
|
<Button
|
|
button="alt"
|
|
label={__('Controversial')}
|
|
icon={ICONS.CONTROVERSIAL}
|
|
iconSize={18}
|
|
onClick={() => setSort(SORT_COMMENTS_CONTROVERSIAL)}
|
|
className={classnames(`button-toggle`, {
|
|
'button-toggle--active': sort === SORT_COMMENTS_CONTROVERSIAL,
|
|
})}
|
|
/>
|
|
<Button
|
|
button="alt"
|
|
label={__('New')}
|
|
icon={ICONS.NEW}
|
|
iconSize={18}
|
|
onClick={() => setSort(SORT_COMMENTS_NEW)}
|
|
className={classnames(`button-toggle`, {
|
|
'button-toggle--active': sort === SORT_COMMENTS_NEW,
|
|
})}
|
|
/>
|
|
</span>
|
|
)}
|
|
<Button
|
|
button="alt"
|
|
icon={ICONS.REFRESH}
|
|
title={__('Refresh')}
|
|
onClick={() => {
|
|
fetchComments(uri);
|
|
fetchReacts(uri);
|
|
}}
|
|
/>
|
|
</>
|
|
}
|
|
actions={
|
|
<>
|
|
<CommentCreate uri={uri} />
|
|
|
|
{!isFetchingComments && hasNoComments && <Empty text={__('That was pretty deep. What do you think?')} />}
|
|
|
|
<ul className="comments" ref={commentRef}>
|
|
{comments &&
|
|
displayedComments &&
|
|
displayedComments.map(comment => {
|
|
return (
|
|
<CommentView
|
|
isTopLevel
|
|
threadDepth={3}
|
|
key={comment.comment_id}
|
|
uri={uri}
|
|
authorUri={comment.channel_url}
|
|
author={comment.channel_name}
|
|
claimId={comment.claim_id}
|
|
commentId={comment.comment_id}
|
|
message={comment.comment}
|
|
timePosted={comment.timestamp * 1000}
|
|
claimIsMine={claimIsMine}
|
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
|
linkedComment={linkedComment}
|
|
isPinned={comment.is_pinned}
|
|
/>
|
|
);
|
|
})}
|
|
</ul>
|
|
|
|
{(isFetchingComments || moreBelow) && (
|
|
<div className="main--empty" ref={spinnerRef}>
|
|
<Spinner type="small" />
|
|
</div>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default CommentList;
|