lbry-desktop/ui/page/ownComments/view.jsx
infinite-persistence de6c6f9bfd
List own comments (#7171)
* Add option to pass in url-search params.

Impetus: allow linked comment ID and setting the discussion tab when clicking on the `ClaimPreview`.

* comment.list: fix typos and renamed variables

- Switch from 'author' to 'creator' to disambiguate between comment author and content author. For comment author, we'll use 'commenter' from now on.
- Corrected 'commenterClaimId' to 'creatorClaimId' (just a typo, no functional change).

* doCommentReset: change param from uri to claimId

This reduces one lookup as clients will always have the claimID ready, but might not have the full URI.

It was using URI previously just to match the other APIs.

* Add doCommentListOwn -- command to fetch own comments

Since the redux slice is set up based on content or channel ID (for Channel Discussion page), re-use the channel ID for the case of "own comments". We always clear each ID when fetching page-0, so no worries of conflict when actually browsing the Channel Discussion page.

* Comment: add option to hide the actions section

* Implement own-comments page

* Use new param to remove sort-pins-first.

comment.List currently always pushes pins to the top to support pagination. This new param removes this behavior.
2021-10-01 08:10:27 -04:00

210 lines
6.9 KiB
JavaScript

// @flow
import React from 'react';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import ClaimPreview from 'component/claimPreview';
import Comment from 'component/comment';
import Card from 'component/common/card';
import Empty from 'component/common/empty';
import Page from 'component/page';
import Spinner from 'component/spinner';
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
import * as ICONS from 'constants/icons';
import useFetched from 'effects/use-fetched';
import debounce from 'util/debounce';
function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) {
return Math.ceil(value / devicePixelRatio);
}
return Math.ceil(value * devicePixelRatio);
}
type Props = {
activeChannelClaim: ?ChannelClaim,
allComments: Array<Comment>,
totalComments: number,
topLevelTotalPages: number,
isFetchingComments: boolean,
claimsById: any,
doCommentReset: (claimId: string) => void,
doCommentListOwn: (channelId: string, page: number, pageSize: number) => void,
};
export default function OwnComments(props: Props) {
const {
activeChannelClaim,
allComments,
totalComments,
isFetchingComments,
claimsById,
doCommentReset,
doCommentListOwn,
} = props;
const spinnerRef = React.useRef();
const [page, setPage] = React.useState(0);
const [activeChannelId, setActiveChannelId] = React.useState('');
// Since we are sharing the key for Discussion and MyComments, don't show
// the list until we've gone through the initial reset.
const wasResetAndReady = useFetched(isFetchingComments);
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
const moreBelow = page < totalPages;
function getCommentsElem(comments) {
return comments.map((comment) => {
const contentClaim = claimsById[comment.claim_id];
const isChannel = contentClaim && contentClaim.value_type === 'channel';
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
return (
<div key={comment.comment_id} className="comments-own card__main-actions">
<div className="section__actions">
<div className="comments-own--claim">
{contentClaim && (
<ClaimPreview
uri={contentClaim.canonical_url}
searchParams={{
...(isChannel ? { view: 'discussion' } : {}),
...(isLivestream ? {} : { lc: comment.comment_id }),
}}
hideActions
hideMenu
properties={() => null}
/>
)}
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
</div>
<Comment
isTopLevel
hideActions
authorUri={comment.channel_url}
author={comment.channel_name}
commentId={comment.comment_id}
message={comment.comment}
timePosted={comment.timestamp * 1000}
commentIsMine
supportAmount={comment.support_amount}
numDirectReplies={0} // Don't show replies here
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
/>
</div>
</div>
);
});
}
// Active channel changed
React.useEffect(() => {
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
setActiveChannelId(activeChannelClaim.claim_id);
setPage(0);
}
}, [activeChannelClaim, activeChannelId]);
// Reset comments
React.useEffect(() => {
if (page === 0 && activeChannelId) {
doCommentReset(activeChannelId);
setPage(1);
}
}, [page, activeChannelId]); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch own comments
React.useEffect(() => {
if (page !== 0 && activeChannelId) {
doCommentListOwn(activeChannelId, page, COMMENT_PAGE_SIZE_TOP_LEVEL);
}
}, [page]); // eslint-disable-line react-hooks/exhaustive-deps
// Infinite scroll
React.useEffect(() => {
function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) {
if (!spinnerRef || !spinnerRef.current) {
return false;
}
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
const windowH = window.innerHeight || document.documentElement.clientHeight; // $FlowFixMe
const windowW = window.innerWidth || document.documentElement.clientWidth; // $FlowFixMe
const isApproachingViewport = yPrefetchPx !== 0 && rect.top < windowH + scaleToDevicePixelRatio(yPrefetchPx);
const isInViewport =
rect.width > 0 &&
rect.height > 0 &&
rect.bottom >= 0 &&
rect.right >= 0 &&
// $FlowFixMe
rect.top <= windowH &&
// $FlowFixMe
rect.left <= windowW;
return (isInViewport || isApproachingViewport) && page < topLevelTotalPages;
}
const handleCommentScroll = debounce(() => {
if (shouldFetchNextPage(page, totalPages, window, document)) {
setPage(page + 1);
}
}, 200);
if (!isFetchingComments && moreBelow && spinnerRef && spinnerRef.current) {
if (shouldFetchNextPage(page, totalPages, window, document, 0)) {
setPage(page + 1);
} else {
window.addEventListener('scroll', handleCommentScroll);
return () => window.removeEventListener('scroll', handleCommentScroll);
}
}
}, [page, spinnerRef, isFetchingComments, moreBelow, totalPages]);
// **************************************************************************
// **************************************************************************
if (!activeChannelClaim) {
return null;
}
return (
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
<ChannelSelector hideAnon />
<Card
isBodyList
title={
totalComments > 0
? totalComments === 1
? __('1 comment')
: __('%total_comments% comments', { total_comments: totalComments })
: isFetchingComments
? ''
: __('No comments')
}
titleActions={
<Button
button="alt"
icon={ICONS.REFRESH}
title={__('Refresh')}
onClick={() => {
setPage(0);
}}
/>
}
body={
<>
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
{(isFetchingComments || moreBelow) && (
<div className="main--empty" ref={spinnerRef}>
<Spinner type="small" />
</div>
)}
</>
}
/>
</Page>
);
}