Comments Pagination
## Issue 6158 - Support Comment Pagination
This commit is contained in:
parent
fba8b89b3b
commit
0cf6fe3df5
23 changed files with 793 additions and 269 deletions
65
flow-typed/Comment.js
vendored
65
flow-typed/Comment.js
vendored
|
@ -13,6 +13,7 @@ declare type Comment = {
|
|||
parent_id?: number, // comment_id of comment this is in reply to
|
||||
is_pinned: boolean,
|
||||
support_amount: number,
|
||||
replies: number, // number of direct replies (i.e. excluding nested replies).
|
||||
};
|
||||
|
||||
declare type PerChannelSettings = {
|
||||
|
@ -27,15 +28,21 @@ declare type PerChannelSettings = {
|
|||
declare type CommentsState = {
|
||||
commentsByUri: { [string]: string },
|
||||
superChatsByUri: { [string]: { totalAmount: number, comments: Array<Comment> } },
|
||||
byId: { [string]: Array<string> },
|
||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
||||
byId: { [string]: Array<string> }, // ClaimID -> list of fetched comment IDs.
|
||||
totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron.
|
||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of fetched replies.
|
||||
totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron.
|
||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of fetched top level comments.
|
||||
topLevelTotalPagesById: { [string]: number }, // ClaimID -> total number of top-level pages in commentron. Based on COMMENT_PAGE_SIZE_TOP_LEVEL.
|
||||
topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron.
|
||||
commentById: { [string]: Comment },
|
||||
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||
isLoading: boolean,
|
||||
isLoadingByParentId: { [string]: boolean },
|
||||
myComments: ?Set<string>,
|
||||
isFetchingReacts: boolean,
|
||||
myReactsByCommentId: any,
|
||||
othersReactsByCommentId: any,
|
||||
myReactsByCommentId: ?{ [string]: Array<string> }, // {"CommentId:MyChannelId": ["like", "dislike", ...]}
|
||||
othersReactsByCommentId: ?{ [string]: { [string]: number } }, // {"CommentId:MyChannelId": {"like": 2, "dislike": 2, ...}}
|
||||
pendingCommentReactions: Array<string>,
|
||||
moderationBlockList: ?Array<string>, // @KP rename to "personalBlockList"?
|
||||
adminBlockList: ?Array<string>,
|
||||
|
@ -64,17 +71,47 @@ declare type CommentReactParams = {
|
|||
remove?: boolean,
|
||||
};
|
||||
|
||||
declare type CommentReactListParams = {
|
||||
comment_ids?: string,
|
||||
channel_id?: string,
|
||||
channel_name?: string,
|
||||
wallet_id?: string,
|
||||
react_types?: string,
|
||||
};
|
||||
|
||||
declare type CommentListParams = {
|
||||
page: number,
|
||||
page_size: number,
|
||||
claim_id: string,
|
||||
page: number, // pagination: which page of results
|
||||
page_size: number, // pagination: nr of comments to show in a page (max 200)
|
||||
claim_id: string, // claim id of claim being commented on
|
||||
channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check)
|
||||
channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check)
|
||||
author_claim_id?: string, // filters comments to just this author
|
||||
parent_id?: string, // filters comments to those under this thread
|
||||
top_level?: boolean, // filters to only top level comments
|
||||
hidden?: boolean, // if true, will show hidden comments as well
|
||||
sort_by?: number, // NEWEST=0, OLDEST=1, CONTROVERSY=2, POPULARITY=3,
|
||||
};
|
||||
|
||||
declare type CommentListResponse = {
|
||||
items: Array<Comment>,
|
||||
total_amount: number,
|
||||
page: number,
|
||||
page_size: number,
|
||||
total_items: number, // Grand total for the claim being commented on.
|
||||
total_filtered_items: number, // Total for filtered queries (e.g. top_level=true, parent_id=xxx, etc.).
|
||||
total_pages: number,
|
||||
has_hidden_comments: boolean,
|
||||
};
|
||||
|
||||
declare type CommentByIdParams = {
|
||||
comment_id: string,
|
||||
with_ancestors: boolean,
|
||||
}
|
||||
|
||||
declare type CommentByIdResponse = {
|
||||
items: Comment,
|
||||
ancestors: Array<Comment>,
|
||||
}
|
||||
|
||||
declare type CommentAbandonParams = {
|
||||
comment_id: string,
|
||||
creator_channel_id?: string,
|
||||
|
@ -94,6 +131,16 @@ declare type CommentCreateParams = {
|
|||
|
||||
declare type SuperListParams = {};
|
||||
|
||||
declare type SuperListResponse = {
|
||||
page: number,
|
||||
page_size: number,
|
||||
total_pages: number,
|
||||
total_items: number,
|
||||
total_amount: number,
|
||||
items: Array<Comment>,
|
||||
has_hidden_comments: boolean,
|
||||
};
|
||||
|
||||
declare type ModerationBlockParams = {};
|
||||
|
||||
declare type ModerationAddDelegateParams = {
|
||||
|
|
|
@ -17,6 +17,7 @@ const Comments = {
|
|||
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
||||
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
||||
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params),
|
||||
comment_by_id: (params: CommentByIdParams) => fetchCommentsApi('comment.ByID', params),
|
||||
setting_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params),
|
||||
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
|
||||
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import ChannelDiscussion from './view';
|
||||
import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
|
||||
|
@ -8,10 +7,9 @@ import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
|
|||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const linkedCommentId = urlParams.get('lc');
|
||||
|
||||
return {
|
||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
||||
linkedCommentId: urlParams.get('lc'),
|
||||
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,19 +5,19 @@ import Empty from 'component/common/empty';
|
|||
|
||||
type Props = {
|
||||
uri: string,
|
||||
linkedComment: ?any,
|
||||
linkedCommentId?: string,
|
||||
commentsDisabled: boolean,
|
||||
};
|
||||
|
||||
function ChannelDiscussion(props: Props) {
|
||||
const { uri, linkedComment, commentsDisabled } = props;
|
||||
const { uri, linkedCommentId, commentsDisabled } = props;
|
||||
|
||||
if (commentsDisabled) {
|
||||
return <Empty text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
return (
|
||||
<section className="section">
|
||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,43 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri, makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { doCommentUpdate } from 'redux/actions/comments';
|
||||
import {
|
||||
makeSelectStakedLevelForChannelUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
selectMyChannelClaims,
|
||||
} from 'lbry-redux';
|
||||
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectLinkedCommentAncestors, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
||||
import { selectActiveChannelId, selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import Comment from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const activeChannelId = selectActiveChannelId(state);
|
||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||
|
||||
return {
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
||||
fetchReplies: (uri, parentId, page, pageSize, sortBy) =>
|
||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
@ -36,8 +37,10 @@ type Props = {
|
|||
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
||||
updateComment: (string, string) => void,
|
||||
fetchReplies: (string, string, number, number, number) => void,
|
||||
commentModBlock: (string) => void,
|
||||
linkedComment?: any,
|
||||
linkedCommentId?: string,
|
||||
linkedCommentAncestors: { [string]: Array<string> },
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
commentingEnabled: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
|
@ -53,6 +56,7 @@ type Props = {
|
|||
playingUri: ?PlayingUri,
|
||||
stakedLevel: number,
|
||||
supportAmount: number,
|
||||
numDirectReplies: number,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -71,7 +75,9 @@ function Comment(props: Props) {
|
|||
commentIsMine,
|
||||
commentId,
|
||||
updateComment,
|
||||
linkedComment,
|
||||
fetchReplies,
|
||||
linkedCommentId,
|
||||
linkedCommentAncestors,
|
||||
commentingEnabled,
|
||||
myChannels,
|
||||
doToast,
|
||||
|
@ -82,18 +88,23 @@ function Comment(props: Props) {
|
|||
playingUri,
|
||||
stakedLevel,
|
||||
supportAmount,
|
||||
numDirectReplies,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
push,
|
||||
replace,
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const [editedMessage, setCommentValue] = useState(message);
|
||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
||||
// used for controlling the visibility of the menu icon
|
||||
const [mouseIsHovering, setMouseHover] = useState(false);
|
||||
const [showReplies, setShowReplies] = useState(false);
|
||||
const [page, setPage] = useState(0);
|
||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
||||
const hasChannels = myChannels && myChannels.length > 0;
|
||||
|
@ -111,6 +122,18 @@ function Comment(props: Props) {
|
|||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Auto-expand (limited to linked-comments for now, but can be for all)
|
||||
useEffect(() => {
|
||||
if (
|
||||
linkedCommentId &&
|
||||
linkedCommentAncestors[linkedCommentId] &&
|
||||
linkedCommentAncestors[linkedCommentId].includes(commentId)
|
||||
) {
|
||||
setShowReplies(true);
|
||||
setPage(1);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
setCharCount(editedMessage.length);
|
||||
|
@ -131,6 +154,12 @@ function Comment(props: Props) {
|
|||
}
|
||||
}, [author, authorUri, editedMessage, isEditing, setEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page > 0) {
|
||||
fetchReplies(uri, commentId, page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST);
|
||||
}
|
||||
}, [page, uri, commentId, fetchReplies]);
|
||||
|
||||
function handleEditMessageChanged(event) {
|
||||
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
|
||||
}
|
||||
|
@ -176,7 +205,7 @@ function Comment(props: Props) {
|
|||
>
|
||||
<div
|
||||
className={classnames('comment__content', {
|
||||
'comment--highlighted': linkedComment && linkedComment.comment_id === commentId,
|
||||
'comment--highlighted': linkedCommentId && linkedCommentId === commentId,
|
||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
|
@ -302,13 +331,43 @@ function Comment(props: Props) {
|
|||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||
</div>
|
||||
|
||||
{numDirectReplies > 0 && !showReplies && (
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
label={
|
||||
numDirectReplies < 2
|
||||
? __('Show reply')
|
||||
: __('Show %count% replies', { count: numDirectReplies })
|
||||
}
|
||||
button="link"
|
||||
onClick={() => {
|
||||
setShowReplies(true);
|
||||
if (page === 0) {
|
||||
setPage(1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{numDirectReplies > 0 && showReplies && (
|
||||
<div className="comment__actions">
|
||||
<Button label={__('Hide replies')} button="link" onClick={() => setShowReplies(false)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isReplying && (
|
||||
<CommentCreate
|
||||
isReply
|
||||
uri={uri}
|
||||
parentId={commentId}
|
||||
onDoneReplying={() => setReplying(false)}
|
||||
onCancelReplying={() => setReplying(false)}
|
||||
onDoneReplying={() => {
|
||||
setShowReplies(true);
|
||||
setReplying(false);
|
||||
}}
|
||||
onCancelReplying={() => {
|
||||
setReplying(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -317,7 +376,16 @@ function Comment(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<CommentsReplies threadDepth={threadDepth - 1} uri={uri} parentId={commentId} linkedComment={linkedComment} />
|
||||
{showReplies && (
|
||||
<CommentsReplies
|
||||
threadDepth={threadDepth - 1}
|
||||
uri={uri}
|
||||
parentId={commentId}
|
||||
linkedCommentId={linkedCommentId}
|
||||
numDirectReplies={numDirectReplies}
|
||||
onShowMore={() => setPage(page + 1)}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ type Props = {
|
|||
toast: (string) => void,
|
||||
claimIsMine: boolean,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
justCommented: Array<string>,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
|
@ -54,7 +53,6 @@ export function CommentCreate(props: Props) {
|
|||
livestream,
|
||||
claimIsMine,
|
||||
sendTip,
|
||||
justCommented,
|
||||
} = props;
|
||||
const buttonref: ElementRef<any> = React.useRef();
|
||||
const {
|
||||
|
@ -153,7 +151,6 @@ export function CommentCreate(props: Props) {
|
|||
setIsReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setCommentFailure(false);
|
||||
justCommented.push(res.comment_id);
|
||||
|
||||
if (onDoneReplying) {
|
||||
onDoneReplying();
|
||||
|
@ -217,7 +214,13 @@ export function CommentCreate(props: Props) {
|
|||
autoFocus
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
label={isSubmitting ? __('Sending...') : (commentFailure && tipAmount === successTip.tipAmount) ? __('Re-submit') : __('Send')}
|
||||
label={
|
||||
isSubmitting
|
||||
? __('Sending...')
|
||||
: commentFailure && tipAmount === successTip.tipAmount
|
||||
? __('Re-submit')
|
||||
: __('Send')
|
||||
}
|
||||
onClick={handleSupportComment}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsReviewingSupportComment(false)} />
|
||||
|
|
|
@ -3,7 +3,6 @@ import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectC
|
|||
import {
|
||||
doCommentAbandon,
|
||||
doCommentPin,
|
||||
doCommentList,
|
||||
doCommentModBlock,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModBlockAsModerator,
|
||||
|
@ -30,8 +29,7 @@ const perform = (dispatch) => ({
|
|||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
|
||||
muteChannel: (channelUri) => dispatch(doChannelMute(channelUri)),
|
||||
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
|
||||
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
||||
pinComment: (commentId, claimId, remove) => dispatch(doCommentPin(commentId, claimId, remove)),
|
||||
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
||||
commentModBlock: (commenterUri) => dispatch(doCommentModBlock(commenterUri)),
|
||||
commentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
|
||||
|
|
|
@ -7,18 +7,15 @@ import Icon from 'component/common/icon';
|
|||
import { parseURI } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: ?Claim,
|
||||
clearPlayingUri: () => void,
|
||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
||||
commentId: string, // sha256 digest identifying the comment
|
||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
||||
deleteComment: (string, ?string) => void,
|
||||
linkedComment?: any,
|
||||
isPinned: boolean,
|
||||
pinComment: (string, boolean) => Promise<any>,
|
||||
pinComment: (string, string, boolean) => Promise<any>,
|
||||
muteChannel: (string) => void,
|
||||
fetchComments: (string) => void,
|
||||
handleEditComment: () => void,
|
||||
contentChannelPermanentUrl: any,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
|
@ -35,7 +32,6 @@ type Props = {
|
|||
|
||||
function CommentMenuList(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
claim,
|
||||
authorUri,
|
||||
commentIsMine,
|
||||
|
@ -49,7 +45,6 @@ function CommentMenuList(props: Props) {
|
|||
isTopLevel,
|
||||
isPinned,
|
||||
handleEditComment,
|
||||
fetchComments,
|
||||
commentModBlock,
|
||||
commentModBlockAsAdmin,
|
||||
commentModBlockAsModerator,
|
||||
|
@ -77,8 +72,8 @@ function CommentMenuList(props: Props) {
|
|||
activeModeratorInfo &&
|
||||
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
|
||||
|
||||
function handlePinComment(commentId, remove) {
|
||||
pinComment(commentId, remove).then(() => fetchComments(uri));
|
||||
function handlePinComment(commentId, claimId, remove) {
|
||||
pinComment(commentId, claimId, remove);
|
||||
}
|
||||
|
||||
function handleDeleteComment() {
|
||||
|
@ -122,7 +117,7 @@ function CommentMenuList(props: Props) {
|
|||
{activeChannelIsCreator && isTopLevel && (
|
||||
<MenuItem
|
||||
className="comment__menu-option menu__link"
|
||||
onSelect={isPinned ? () => handlePinComment(commentId, true) : () => handlePinComment(commentId, false)}
|
||||
onSelect={() => handlePinComment(commentId, claim ? claim.claim_id : '', isPinned)}
|
||||
>
|
||||
<span className={'button__content'}>
|
||||
<Icon aria-hidden icon={ICONS.PIN} className={'icon'} />
|
||||
|
|
|
@ -6,17 +6,22 @@ import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment }
|
|||
import { doCommentReact } from 'redux/actions/comments';
|
||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
myReacts: makeSelectMyReactionsForComment(props.commentId)(state),
|
||||
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
||||
activeChannelId: selectActiveChannelId(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const activeChannelId = selectActiveChannelId(state);
|
||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||
|
||||
const perform = dispatch => ({
|
||||
return {
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
myReacts: makeSelectMyReactionsForComment(reactionKey)(state),
|
||||
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
||||
activeChannelId,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
react: (commentId, type) => dispatch(doCommentReact(commentId, type)),
|
||||
doToast: params => dispatch(doToast(params)),
|
||||
doToast: (params) => dispatch(doToast(params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Comment);
|
||||
|
|
|
@ -2,32 +2,42 @@ import { connect } from 'react-redux';
|
|||
import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectTopLevelCommentsForUri,
|
||||
makeSelectTopLevelTotalPagesForUri,
|
||||
selectIsFetchingComments,
|
||||
makeSelectTotalCommentsCountForUri,
|
||||
selectOthersReactsById,
|
||||
makeSelectCommentsDisabledForUri,
|
||||
selectMyReactionsByCommentId,
|
||||
makeSelectCommentIdsForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
|
||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
||||
import CommentsList from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
reactionsById: selectOthersReactsById(state),
|
||||
activeChannelId: selectActiveChannelId(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
myReactsByCommentId: selectMyReactionsByCommentId(state),
|
||||
othersReactsById: selectOthersReactsById(state),
|
||||
activeChannelId: selectActiveChannelId(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
||||
fetchReacts: (uri) => dispatch(doCommentReactList(uri)),
|
||||
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||
resetComments: (uri) => dispatch(doCommentReset(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentsList);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @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 { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import CommentView from 'component/comment';
|
||||
|
@ -11,59 +11,70 @@ 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';
|
||||
import debounce from 'util/debounce';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 50;
|
||||
|
||||
type Props = {
|
||||
comments: Array<Comment>,
|
||||
allCommentIds: any,
|
||||
topLevelComments: Array<Comment>,
|
||||
topLevelTotalPages: number,
|
||||
commentsDisabledBySettings: boolean,
|
||||
fetchComments: (string) => void,
|
||||
fetchReacts: (string) => Promise<any>,
|
||||
fetchTopLevelComments: (string, number, number, number) => void,
|
||||
fetchComment: (string) => void,
|
||||
fetchReacts: (Array<string>) => Promise<any>,
|
||||
resetComments: (string) => void,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
isFetchingComments: boolean,
|
||||
linkedComment: any,
|
||||
linkedCommentId?: string,
|
||||
totalComments: number,
|
||||
fetchingChannels: boolean,
|
||||
reactionsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
||||
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||
activeChannelId: ?string,
|
||||
};
|
||||
|
||||
function CommentList(props: Props) {
|
||||
const {
|
||||
fetchComments,
|
||||
allCommentIds,
|
||||
fetchTopLevelComments,
|
||||
fetchComment,
|
||||
fetchReacts,
|
||||
resetComments,
|
||||
uri,
|
||||
comments,
|
||||
topLevelComments,
|
||||
topLevelTotalPages,
|
||||
commentsDisabledBySettings,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
isFetchingComments,
|
||||
linkedComment,
|
||||
linkedCommentId,
|
||||
totalComments,
|
||||
fetchingChannels,
|
||||
reactionsById,
|
||||
myReactsByCommentId,
|
||||
othersReactsById,
|
||||
activeChannelId,
|
||||
} = 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 DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||
const [page, setPage] = React.useState(0);
|
||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||
|
||||
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
|
||||
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
||||
);
|
||||
const [justCommented] = React.useState([]);
|
||||
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
||||
|
||||
const hasNoComments = !totalComments;
|
||||
const moreBelow = totalComments - end > 0;
|
||||
const moreBelow = page < topLevelTotalPages;
|
||||
|
||||
const isMyComment = (channelId: string): boolean => {
|
||||
if (myChannels != null && channelId != null) {
|
||||
for (let i = 0; i < myChannels.length; i++) {
|
||||
|
@ -75,86 +86,106 @@ function CommentList(props: Props) {
|
|||
return false;
|
||||
};
|
||||
|
||||
const handleMoreBelow = React.useCallback(() => {
|
||||
if (moreBelow) {
|
||||
setEnd(end + 10);
|
||||
function changeSort(newSort) {
|
||||
if (sort !== newSort) {
|
||||
setSort(newSort);
|
||||
setPage(0); // Invalidate existing comments
|
||||
}
|
||||
}, [end, setEnd, moreBelow]);
|
||||
}
|
||||
|
||||
// Reset comments
|
||||
useEffect(() => {
|
||||
fetchComments(uri);
|
||||
}, [fetchComments, uri]);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalComments && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
||||
fetchReacts(uri)
|
||||
.then(() => {
|
||||
setReadyToDisplayComments(true);
|
||||
})
|
||||
.catch(() => setReadyToDisplayComments(true));
|
||||
if (page === 0) {
|
||||
resetComments(uri);
|
||||
setPage(1);
|
||||
}
|
||||
}, [fetchReacts, uri, totalComments, activeChannelId, fetchingChannels]);
|
||||
}, [page, uri, resetComments]);
|
||||
|
||||
// Fetch top-level comments
|
||||
useEffect(() => {
|
||||
if (page !== 0) {
|
||||
if (page === 1 && linkedCommentId) {
|
||||
fetchComment(linkedCommentId);
|
||||
}
|
||||
|
||||
fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
||||
}
|
||||
}, [fetchTopLevelComments, uri, page, resetComments, sort, linkedCommentId, fetchComment]);
|
||||
|
||||
// Fetch reacts
|
||||
useEffect(() => {
|
||||
if (totalFetchedComments > 0 && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
||||
let idsForReactionFetch;
|
||||
|
||||
if (!othersReactsById || !myReactsByCommentId) {
|
||||
idsForReactionFetch = allCommentIds;
|
||||
} else {
|
||||
idsForReactionFetch = allCommentIds.filter((commentId) => {
|
||||
const key = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
||||
return !othersReactsById[key] || !myReactsByCommentId[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (idsForReactionFetch.length !== 0) {
|
||||
fetchReacts(idsForReactionFetch)
|
||||
.then(() => {
|
||||
setReadyToDisplayComments(true);
|
||||
})
|
||||
.catch(() => setReadyToDisplayComments(true));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
totalFetchedComments,
|
||||
allCommentIds,
|
||||
othersReactsById,
|
||||
myReactsByCommentId,
|
||||
fetchReacts,
|
||||
uri,
|
||||
activeChannelId,
|
||||
fetchingChannels,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) {
|
||||
commentRef.current.scrollIntoView({ block: 'start' });
|
||||
window.scrollBy(0, -100);
|
||||
window.scrollBy(0, -125);
|
||||
}
|
||||
}, [readyToDisplayComments, linkedCommentId]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleCommentScroll(e) {
|
||||
const handleCommentScroll = debounce(() => {
|
||||
// $FlowFixMe
|
||||
const rect = spinnerRef.current.getBoundingClientRect();
|
||||
|
||||
const isInViewport =
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
// $FlowFixMe
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
// $FlowFixMe
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
||||
rect.left <= (window.innerWidth || document.documentElement.clientWidth);
|
||||
|
||||
if (isInViewport) {
|
||||
handleMoreBelow();
|
||||
if (isInViewport && page < topLevelTotalPages) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}
|
||||
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
||||
|
||||
if (!isFetchingComments && readyToDisplayComments && moreBelow && spinnerRef && spinnerRef.current) {
|
||||
window.addEventListener('scroll', handleCommentScroll);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('scroll', handleCommentScroll);
|
||||
}, [moreBelow, handleMoreBelow, spinnerRef, isFetchingComments, readyToDisplayComments]);
|
||||
}, [
|
||||
page,
|
||||
moreBelow,
|
||||
spinnerRef,
|
||||
isFetchingComments,
|
||||
readyToDisplayComments,
|
||||
topLevelComments.length,
|
||||
topLevelTotalPages,
|
||||
]);
|
||||
|
||||
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, justCommented })
|
||||
: [];
|
||||
const displayedComments = readyToDisplayComments
|
||||
? prepareComments(sortedComments, linkedComment).slice(start, end)
|
||||
: [];
|
||||
const displayedComments = readyToDisplayComments ? topLevelComments : [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -174,9 +205,9 @@ function CommentList(props: Props) {
|
|||
label={__('Best')}
|
||||
icon={ICONS.BEST}
|
||||
iconSize={18}
|
||||
onClick={() => setSort(SORT_COMMENTS_BEST)}
|
||||
onClick={() => changeSort(SORT_BY.POPULARITY)}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': sort === SORT_COMMENTS_BEST,
|
||||
'button-toggle--active': sort === SORT_BY.POPULARITY,
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
|
@ -184,9 +215,9 @@ function CommentList(props: Props) {
|
|||
label={__('Controversial')}
|
||||
icon={ICONS.CONTROVERSIAL}
|
||||
iconSize={18}
|
||||
onClick={() => setSort(SORT_COMMENTS_CONTROVERSIAL)}
|
||||
onClick={() => changeSort(SORT_BY.CONTROVERSY)}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': sort === SORT_COMMENTS_CONTROVERSIAL,
|
||||
'button-toggle--active': sort === SORT_BY.CONTROVERSY,
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
|
@ -194,9 +225,9 @@ function CommentList(props: Props) {
|
|||
label={__('New')}
|
||||
icon={ICONS.NEW}
|
||||
iconSize={18}
|
||||
onClick={() => setSort(SORT_COMMENTS_NEW)}
|
||||
onClick={() => changeSort(SORT_BY.NEWEST)}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': sort === SORT_COMMENTS_NEW,
|
||||
'button-toggle--active': sort === SORT_BY.NEWEST,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
|
@ -206,22 +237,21 @@ function CommentList(props: Props) {
|
|||
icon={ICONS.REFRESH}
|
||||
title={__('Refresh')}
|
||||
onClick={() => {
|
||||
fetchComments(uri);
|
||||
fetchReacts(uri);
|
||||
setPage(0);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<CommentCreate uri={uri} justCommented={justCommented} />
|
||||
<CommentCreate uri={uri} />
|
||||
|
||||
{!commentsDisabledBySettings && !isFetchingComments && hasNoComments && (
|
||||
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
||||
)}
|
||||
|
||||
<ul className="comments" ref={commentRef}>
|
||||
{comments &&
|
||||
{topLevelComments &&
|
||||
displayedComments &&
|
||||
displayedComments.map((comment) => {
|
||||
return (
|
||||
|
@ -238,9 +268,10 @@ function CommentList(props: Props) {
|
|||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
linkedCommentId={linkedCommentId}
|
||||
isPinned={comment.is_pinned}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { makeSelectRepliesForParentId } from 'redux/selectors/comments';
|
||||
import { selectIsFetchingCommentsByParentId, makeSelectRepliesForParentId } from 'redux/selectors/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import CommentsReplies from './view';
|
||||
|
||||
|
@ -9,6 +9,7 @@ const select = (state, props) => ({
|
|||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
||||
});
|
||||
|
||||
export default connect(select)(CommentsReplies);
|
||||
|
|
|
@ -3,32 +3,42 @@ import * as ICONS from 'constants/icons';
|
|||
import React from 'react';
|
||||
import Comment from 'component/comment';
|
||||
import Button from 'component/button';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
comments: Array<any>,
|
||||
uri: string,
|
||||
parentId: string,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
linkedComment?: Comment,
|
||||
linkedCommentId?: string,
|
||||
commentingEnabled: boolean,
|
||||
threadDepth: number,
|
||||
numDirectReplies: number,
|
||||
isFetchingByParentId: { [string]: boolean },
|
||||
onShowMore?: () => void,
|
||||
};
|
||||
|
||||
function CommentsReplies(props: Props) {
|
||||
const { uri, comments, claimIsMine, myChannels, linkedComment, commentingEnabled, threadDepth } = props;
|
||||
const {
|
||||
uri,
|
||||
parentId,
|
||||
comments,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
linkedCommentId,
|
||||
commentingEnabled,
|
||||
threadDepth,
|
||||
numDirectReplies,
|
||||
isFetchingByParentId,
|
||||
onShowMore,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setExpanded] = React.useState(true);
|
||||
const [start, setStart] = React.useState(0);
|
||||
const [end, setEnd] = React.useState(9);
|
||||
const sortedComments = comments ? [...comments].reverse() : [];
|
||||
const numberOfComments = comments ? comments.length : 0;
|
||||
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
||||
const commentsIndexOfLInked = comments && sortedComments.findIndex((e) => e.comment_id === linkedCommentId);
|
||||
|
||||
function showMore() {
|
||||
if (start > 0) {
|
||||
setStart(0);
|
||||
} else {
|
||||
setEnd(numberOfComments);
|
||||
if (onShowMore) {
|
||||
onShowMore();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,35 +54,12 @@ function CommentsReplies(props: Props) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function handleCommentDone() {
|
||||
if (!isExpanded) {
|
||||
setExpanded(true);
|
||||
setStart(numberOfComments || 0);
|
||||
}
|
||||
setEnd(numberOfComments + 1);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
setStart &&
|
||||
setEnd &&
|
||||
setExpanded &&
|
||||
linkedCommentId &&
|
||||
Number.isInteger(commentsIndexOfLInked) &&
|
||||
commentsIndexOfLInked > -1
|
||||
) {
|
||||
setStart(commentsIndexOfLInked);
|
||||
setEnd(commentsIndexOfLInked + 1);
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [setStart, setEnd, setExpanded, linkedCommentId, commentsIndexOfLInked]);
|
||||
|
||||
const displayedComments = sortedComments.slice(start, end);
|
||||
const displayedComments = comments;
|
||||
|
||||
return (
|
||||
Boolean(numberOfComments) && (
|
||||
Boolean(numDirectReplies) && (
|
||||
<div className="comment__replies-container">
|
||||
{Boolean(numberOfComments) && !isExpanded && (
|
||||
{Boolean(numDirectReplies) && !isExpanded && (
|
||||
<div className="comment__actions--nested">
|
||||
<Button
|
||||
className="comment__action"
|
||||
|
@ -88,7 +75,7 @@ function CommentsReplies(props: Props) {
|
|||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||
|
||||
<ul className="comments--replies">
|
||||
{displayedComments.map((comment, index) => {
|
||||
{displayedComments.map((comment) => {
|
||||
return (
|
||||
<Comment
|
||||
threadDepth={threadDepth}
|
||||
|
@ -102,10 +89,10 @@ function CommentsReplies(props: Props) {
|
|||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
linkedCommentId={linkedCommentId}
|
||||
commentingEnabled={commentingEnabled}
|
||||
handleCommentDone={handleCommentDone}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -113,11 +100,18 @@ function CommentsReplies(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && comments && (end < numberOfComments || start > 0) && (
|
||||
<div className="comment__actions">
|
||||
{isExpanded && comments && displayedComments.length < numDirectReplies && (
|
||||
<div className="comment__actions--nested">
|
||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
||||
</div>
|
||||
)}
|
||||
{isFetchingByParentId[parentId] && (
|
||||
<div className="comment__replies-container">
|
||||
<div className="comment__actions--nested">
|
||||
<Spinner type="small" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ type Props = {
|
|||
embed?: boolean,
|
||||
doCommentSocketConnect: (string, string) => void,
|
||||
doCommentSocketDisconnect: (string) => void,
|
||||
doCommentList: (string, number, number) => void,
|
||||
doCommentList: (string, string, number, number) => void,
|
||||
comments: Array<Comment>,
|
||||
fetchingComments: boolean,
|
||||
doSuperChatList: (string) => void,
|
||||
|
@ -68,7 +68,7 @@ export default function LivestreamComments(props: Props) {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (claimId) {
|
||||
doCommentList(uri, 1, 75);
|
||||
doCommentList(uri, '', 1, 75);
|
||||
doSuperChatList(uri);
|
||||
doCommentSocketConnect(uri, claimId);
|
||||
}
|
||||
|
|
|
@ -248,6 +248,8 @@ export const DISMISS_ERROR = 'DISMISS_ERROR';
|
|||
export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED';
|
||||
export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED';
|
||||
export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED';
|
||||
export const COMMENT_LIST_RESET = 'COMMENT_LIST_RESET';
|
||||
export const COMMENT_BY_ID_COMPLETED = 'COMMENT_BY_ID_COMPLETED';
|
||||
export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED';
|
||||
export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED';
|
||||
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
||||
|
|
|
@ -4,8 +4,18 @@ export const SORT_COMMENTS_NEW = 'new';
|
|||
export const SORT_COMMENTS_BEST = 'best';
|
||||
export const SORT_COMMENTS_CONTROVERSIAL = 'controversial';
|
||||
|
||||
export const SORT_BY = {
|
||||
NEWEST: 0,
|
||||
OLDEST: 1,
|
||||
CONTROVERSY: 2,
|
||||
POPULARITY: 3,
|
||||
};
|
||||
|
||||
export const BLOCK_LEVEL = {
|
||||
SELF: 'self',
|
||||
MODERATOR: 'moderator',
|
||||
ADMIN: 'admin',
|
||||
};
|
||||
|
||||
export const COMMENT_PAGE_SIZE_TOP_LEVEL = 5;
|
||||
export const COMMENT_PAGE_SIZE_REPLIES = 3;
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
|
||||
import FilePage from './view';
|
||||
|
@ -24,11 +23,10 @@ import FilePage from './view';
|
|||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const linkedCommentId = urlParams.get('lc');
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
|
||||
return {
|
||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
||||
linkedCommentId: urlParams.get('lc'),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowMatureContent(state),
|
||||
|
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
renderMode: string,
|
||||
obscureNsfw: boolean,
|
||||
isMature: boolean,
|
||||
linkedComment: any,
|
||||
linkedCommentId?: string,
|
||||
setPrimaryUri: (?string) => void,
|
||||
collection?: Collection,
|
||||
collectionId: string,
|
||||
|
@ -46,7 +46,7 @@ function FilePage(props: Props) {
|
|||
obscureNsfw,
|
||||
isMature,
|
||||
costInfo,
|
||||
linkedComment,
|
||||
linkedCommentId,
|
||||
setPrimaryUri,
|
||||
videoTheaterMode,
|
||||
commentsDisabled,
|
||||
|
@ -146,7 +146,7 @@ function FilePage(props: Props) {
|
|||
<div>
|
||||
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
|
||||
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
||||
{!commentsDisabled && <CommentsList uri={uri} linkedComment={linkedComment} />}
|
||||
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
|
||||
</div>
|
||||
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
|
@ -157,7 +157,7 @@ function FilePage(props: Props) {
|
|||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{isMarkdown && (
|
||||
<div className="file-page__post-comments">
|
||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { BLOCK_LEVEL } from 'constants/comment';
|
||||
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
||||
import { Lbry, parseURI, buildURI, selectClaimsById, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
||||
import {
|
||||
makeSelectCommentIdsForUri,
|
||||
makeSelectMyReactionsForComment,
|
||||
makeSelectOthersReactionsForComment,
|
||||
selectPendingCommentReacts,
|
||||
|
@ -18,7 +17,22 @@ import { selectActiveChannelClaim } from 'redux/selectors/app';
|
|||
import { toHex } from 'util/hex';
|
||||
import Comments from 'comments';
|
||||
|
||||
export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) {
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
function devToast(dispatch, msg) {
|
||||
if (isDev) {
|
||||
console.error(msg); // eslint-disable-line
|
||||
dispatch(doToast({ isError: true, message: `DEV: ${msg}` }));
|
||||
}
|
||||
}
|
||||
|
||||
export function doCommentList(
|
||||
uri: string,
|
||||
parentId: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 99999,
|
||||
sortBy: number = SORT_BY.NEWEST
|
||||
) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const claim = selectClaimsByUri(state)[uri];
|
||||
|
@ -35,6 +49,9 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
|||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_STARTED,
|
||||
data: {
|
||||
parentId,
|
||||
},
|
||||
});
|
||||
|
||||
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
|
||||
|
@ -44,20 +61,28 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
|||
page,
|
||||
claim_id: claimId,
|
||||
page_size: pageSize,
|
||||
parent_id: parentId || undefined,
|
||||
top_level: !parentId,
|
||||
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
|
||||
sort_by: sortBy,
|
||||
})
|
||||
.then((result: CommentListResponse) => {
|
||||
const { items: comments } = result;
|
||||
const { items: comments, total_items, total_filtered_items, total_pages } = result;
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||
data: {
|
||||
comments,
|
||||
parentId,
|
||||
totalItems: total_items,
|
||||
totalFilteredItems: total_filtered_items,
|
||||
totalPages: total_pages,
|
||||
claimId: claimId,
|
||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
uri: uri,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -70,6 +95,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
|||
},
|
||||
});
|
||||
} else {
|
||||
devToast(dispatch, `doCommentList: ${error.message}`);
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||
data: error,
|
||||
|
@ -79,6 +105,51 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
|||
};
|
||||
}
|
||||
|
||||
export function doCommentById(commentId: string) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return Comments.comment_by_id({ comment_id: commentId, with_ancestors: true })
|
||||
.then((result: CommentByIdResponse) => {
|
||||
const { items, ancestors } = result;
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_BY_ID_COMPLETED,
|
||||
data: {
|
||||
comment: items,
|
||||
ancestors: ancestors,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
devToast(dispatch, error.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doCommentReset(uri: string) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const claim = selectClaimsByUri(state)[uri];
|
||||
const claimId = claim ? claim.claim_id : null;
|
||||
|
||||
if (!claimId) {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||
data: 'unable to find claim for uri',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_RESET,
|
||||
data: {
|
||||
claimId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doSuperChatList(uri: string) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
|
@ -97,7 +168,7 @@ export function doSuperChatList(uri: string) {
|
|||
return Comments.super_list({
|
||||
claim_id: claimId,
|
||||
})
|
||||
.then((result: CommentListResponse) => {
|
||||
.then((result: SuperListResponse) => {
|
||||
const { items: comments, total_amount: totalAmount } = result;
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED,
|
||||
|
@ -117,17 +188,16 @@ export function doSuperChatList(uri: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doCommentReactList(uri: string | null, commentId?: string) {
|
||||
export function doCommentReactList(commentIds: Array<string>) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId];
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
|
||||
});
|
||||
|
||||
const params: { comment_ids: string, channel_name?: string, channel_id?: string } = {
|
||||
const params: CommentReactListParams = {
|
||||
comment_ids: commentIds.join(','),
|
||||
};
|
||||
|
||||
|
@ -144,10 +214,12 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
|
|||
data: {
|
||||
myReactions: myReactions || {},
|
||||
othersReactions,
|
||||
channelId: activeChannelClaim ? activeChannelClaim.claim_id : undefined,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
devToast(dispatch, `doCommentReactList: ${error.message}`);
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
||||
data: error,
|
||||
|
@ -182,8 +254,9 @@ export function doCommentReact(commentId: string, type: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
let myReacts = makeSelectMyReactionsForComment(commentId)(state);
|
||||
const othersReacts = makeSelectOthersReactionsForComment(commentId)(state);
|
||||
const reactKey = `${commentId}:${activeChannelClaim.claim_id}`;
|
||||
const myReacts = makeSelectMyReactionsForComment(reactKey)(state);
|
||||
const othersReacts = makeSelectOthersReactionsForComment(reactKey)(state);
|
||||
const params: CommentReactParams = {
|
||||
comment_ids: commentId,
|
||||
channel_name: activeChannelClaim.name,
|
||||
|
@ -217,8 +290,8 @@ export function doCommentReact(commentId: string, type: string) {
|
|||
dispatch({
|
||||
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
|
||||
data: {
|
||||
myReactions: { [commentId]: myReactsObj },
|
||||
othersReactions: { [commentId]: othersReacts },
|
||||
myReactions: { [reactKey]: myReactsObj },
|
||||
othersReactions: { [reactKey]: othersReacts },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -371,7 +444,7 @@ export function doCommentCreate(
|
|||
};
|
||||
}
|
||||
|
||||
export function doCommentPin(commentId: string, remove: boolean) {
|
||||
export function doCommentPin(commentId: string, claimId: string, remove: boolean) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const activeChannel = selectActiveChannelClaim(state);
|
||||
|
@ -394,7 +467,11 @@ export function doCommentPin(commentId: string, remove: boolean) {
|
|||
.then((result: CommentPinResponse) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
||||
data: result,
|
||||
data: {
|
||||
pinnedComment: result.items,
|
||||
claimId,
|
||||
unpin: remove,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
|
@ -3,17 +3,25 @@ import * as ACTIONS from 'constants/action_types';
|
|||
import { handleActions } from 'util/redux-utils';
|
||||
import { BLOCK_LEVEL } from 'constants/comment';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const defaultState: CommentsState = {
|
||||
commentById: {}, // commentId -> Comment
|
||||
byId: {}, // ClaimID -> list of comments
|
||||
repliesByParentId: {}, // ParentCommentID -> list of reply comments
|
||||
topLevelCommentsById: {}, // ClaimID -> list of top level comments
|
||||
byId: {}, // ClaimID -> list of fetched comment IDs.
|
||||
totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron.
|
||||
repliesByParentId: {}, // ParentCommentID -> list of fetched replies.
|
||||
totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron.
|
||||
topLevelCommentsById: {}, // ClaimID -> list of fetched top level comments.
|
||||
topLevelTotalPagesById: {}, // ClaimID -> total number of top-level pages in commentron. Based on COMMENT_PAGE_SIZE_TOP_LEVEL.
|
||||
topLevelTotalCommentsById: {}, // ClaimID -> total top level comments in commentron.
|
||||
// TODO:
|
||||
// Remove commentsByUri
|
||||
// It is not needed and doesn't provide anything but confusion
|
||||
commentsByUri: {}, // URI -> claimId
|
||||
linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||
superChatsByUri: {},
|
||||
isLoading: false,
|
||||
isLoadingByParentId: {},
|
||||
isCommenting: false,
|
||||
myComments: undefined,
|
||||
isFetchingReacts: false,
|
||||
|
@ -39,6 +47,14 @@ const defaultState: CommentsState = {
|
|||
fetchingBlockedWords: false,
|
||||
};
|
||||
|
||||
function pushToArrayInObject(obj, key, valueToPush) {
|
||||
if (!obj[key]) {
|
||||
obj[key] = [valueToPush];
|
||||
} else if (!obj[key].includes(valueToPush)) {
|
||||
obj[key].push(valueToPush);
|
||||
}
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
||||
|
@ -58,10 +74,13 @@ export default handleActions(
|
|||
uri,
|
||||
livestream,
|
||||
}: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data;
|
||||
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
const comments = byId[claimId] || [];
|
||||
const newCommentIds = comments.slice();
|
||||
|
@ -75,12 +94,27 @@ export default handleActions(
|
|||
newCommentIds.unshift(comment.comment_id);
|
||||
byId[claimId] = newCommentIds;
|
||||
|
||||
if (totalCommentsById[claimId]) {
|
||||
totalCommentsById[claimId] += 1;
|
||||
}
|
||||
|
||||
if (comment['parent_id']) {
|
||||
if (!repliesByParentId[comment.parent_id]) {
|
||||
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
||||
} else {
|
||||
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
||||
}
|
||||
|
||||
if (!totalRepliesByParentId[comment.parent_id]) {
|
||||
totalRepliesByParentId[comment.parent_id] = 1;
|
||||
} else {
|
||||
totalRepliesByParentId[comment.parent_id] += 1;
|
||||
}
|
||||
|
||||
// Update the parent's "replies" value
|
||||
if (commentById[comment.parent_id]) {
|
||||
commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
|
||||
}
|
||||
} else {
|
||||
if (!topLevelCommentsById[claimId]) {
|
||||
commentsByUri[uri] = claimId;
|
||||
|
@ -95,8 +129,10 @@ export default handleActions(
|
|||
...state,
|
||||
topLevelCommentsById,
|
||||
repliesByParentId,
|
||||
totalRepliesByParentId,
|
||||
commentById,
|
||||
byId,
|
||||
totalCommentsById,
|
||||
commentsByUri,
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
|
@ -147,12 +183,14 @@ export default handleActions(
|
|||
},
|
||||
|
||||
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||
const { myReactions, othersReactions } = action.data;
|
||||
const { myReactions, othersReactions, channelId } = action.data;
|
||||
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
||||
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
||||
|
||||
if (myReactions) {
|
||||
Object.entries(myReactions).forEach(([commentId, reactions]) => {
|
||||
myReacts[commentId] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
||||
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||
myReacts[key] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
||||
if (count === 1) {
|
||||
acc.push(name);
|
||||
}
|
||||
|
@ -160,9 +198,11 @@ export default handleActions(
|
|||
}, []);
|
||||
});
|
||||
}
|
||||
|
||||
if (othersReactions) {
|
||||
Object.entries(othersReactions).forEach(([commentId, reactions]) => {
|
||||
othersReacts[commentId] = reactions;
|
||||
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||
othersReacts[key] = reactions;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -174,10 +214,32 @@ export default handleActions(
|
|||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
|
||||
[ACTIONS.COMMENT_LIST_STARTED]: (state, action: any) => {
|
||||
const { parentId } = action.data;
|
||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||
if (parentId) {
|
||||
isLoadingByParentId[parentId] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
isLoadingByParentId,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
||||
const { comments, claimId, uri, disabled, authorClaimId } = action.data;
|
||||
const {
|
||||
comments,
|
||||
parentId,
|
||||
totalItems,
|
||||
totalFilteredItems,
|
||||
totalPages,
|
||||
claimId,
|
||||
uri,
|
||||
disabled,
|
||||
authorClaimId,
|
||||
} = action.data;
|
||||
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
|
||||
|
||||
if (disabled) {
|
||||
|
@ -185,10 +247,16 @@ export default handleActions(
|
|||
commentsDisabledChannelIds.push(authorClaimId);
|
||||
}
|
||||
|
||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||
if (parentId) {
|
||||
isLoadingByParentId[parentId] = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
commentsDisabledChannelIds,
|
||||
isLoading: false,
|
||||
isLoadingByParentId,
|
||||
};
|
||||
} else {
|
||||
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
|
||||
|
@ -200,49 +268,130 @@ export default handleActions(
|
|||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||
|
||||
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
||||
// map the comment_ids to the new comments
|
||||
commentById[comment.comment_id] = comment;
|
||||
commentIds[index] = comment.comment_id;
|
||||
};
|
||||
|
||||
const tempRepliesByParent = {};
|
||||
const topLevelComments = [];
|
||||
if (comments) {
|
||||
// we use an Array to preserve order of listing
|
||||
// in reality this doesn't matter and we can just
|
||||
// sort comments by their timestamp
|
||||
const commentIds = Array(comments.length);
|
||||
totalCommentsById[claimId] = totalItems;
|
||||
|
||||
// map the comment_ids to the new comments
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
const comment = comments[i];
|
||||
if (comment['parent_id']) {
|
||||
if (!tempRepliesByParent[comment.parent_id]) {
|
||||
tempRepliesByParent[comment.parent_id] = [comment.comment_id];
|
||||
} else {
|
||||
tempRepliesByParent[comment.parent_id].push(comment.comment_id);
|
||||
}
|
||||
} else {
|
||||
commentById[comment.comment_id] = comment;
|
||||
topLevelComments.push(comment.comment_id);
|
||||
// --- Top-level comments ---
|
||||
if (!parentId) {
|
||||
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
||||
topLevelTotalPagesById[claimId] = totalPages;
|
||||
|
||||
if (!topLevelCommentsById[claimId]) {
|
||||
topLevelCommentsById[claimId] = [];
|
||||
}
|
||||
commentIds[i] = comments[i].comment_id;
|
||||
commentById[commentIds[i]] = comments[i];
|
||||
}
|
||||
topLevelCommentsById[claimId] = topLevelComments;
|
||||
|
||||
byId[claimId] = commentIds;
|
||||
const topLevelCommentIds = topLevelCommentsById[claimId];
|
||||
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
|
||||
if (IS_DEV && comment['parent_id']) console.error('Invalid top-level comment:', comment); // eslint-disable-line
|
||||
|
||||
if (!topLevelCommentIds.includes(comment.comment_id)) {
|
||||
topLevelCommentIds.push(comment.comment_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- Replies ---
|
||||
else {
|
||||
totalRepliesByParentId[parentId] = totalFilteredItems;
|
||||
isLoadingByParentId[parentId] = false;
|
||||
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
|
||||
if (IS_DEV && !comment['parent_id']) console.error('Missing parent_id:', comment); // eslint-disable-line
|
||||
if (IS_DEV && comment.parent_id !== parentId) console.error('Black sheep in the family?:', comment); // eslint-disable-line
|
||||
|
||||
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||
}
|
||||
}
|
||||
|
||||
byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds);
|
||||
commentsByUri[uri] = claimId;
|
||||
}
|
||||
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId, tempRepliesByParent); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||
|
||||
return {
|
||||
...state,
|
||||
topLevelCommentsById,
|
||||
topLevelTotalCommentsById,
|
||||
topLevelTotalPagesById,
|
||||
repliesByParentId,
|
||||
totalCommentsById,
|
||||
totalRepliesByParentId,
|
||||
byId,
|
||||
commentById,
|
||||
commentsByUri,
|
||||
commentsDisabledChannelIds,
|
||||
isLoading: false,
|
||||
isLoadingByParentId,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_BY_ID_COMPLETED]: (state: CommentsState, action: any) => {
|
||||
const { comment, ancestors } = action.data;
|
||||
const claimId = comment.claim_id;
|
||||
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||
const linkedCommentAncestors = Object.assign({}, state.linkedCommentAncestors);
|
||||
|
||||
const updateStore = (comment, commentById, byId, repliesByParentId, topLevelCommentsById) => {
|
||||
// 'comment.ByID' doesn't populate 'replies'. We should have at least 1
|
||||
// at the moment, and the correct value will populated by 'comment.List'.
|
||||
commentById[comment.comment_id] = { ...comment, replies: 1 };
|
||||
byId[claimId] ? byId[claimId].unshift(comment.comment_id) : (byId[claimId] = [comment.comment_id]);
|
||||
|
||||
const parentId = comment.parent_id;
|
||||
if (comment.parent_id) {
|
||||
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||
} else {
|
||||
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||
}
|
||||
};
|
||||
|
||||
updateStore(comment, commentById, byId, repliesByParentId, topLevelCommentsById);
|
||||
|
||||
if (ancestors) {
|
||||
ancestors.forEach((ancestor) => {
|
||||
updateStore(ancestor, commentById, byId, repliesByParentId, topLevelCommentsById);
|
||||
pushToArrayInObject(linkedCommentAncestors, comment.comment_id, ancestor.comment_id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
topLevelCommentsById,
|
||||
topLevelTotalCommentsById,
|
||||
topLevelTotalPagesById,
|
||||
repliesByParentId,
|
||||
byId,
|
||||
commentById,
|
||||
linkedCommentAncestors,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -273,6 +422,51 @@ export default handleActions(
|
|||
isLoading: false,
|
||||
}),
|
||||
|
||||
[ACTIONS.COMMENT_LIST_RESET]: (state: CommentsState, action: any) => {
|
||||
const { claimId } = action.data;
|
||||
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
||||
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
||||
|
||||
function deleteReacts(reactObj, commentIdsToRemove) {
|
||||
if (commentIdsToRemove && commentIdsToRemove.length > 0) {
|
||||
let reactionKeys = Object.keys(reactObj);
|
||||
reactionKeys.forEach((rk) => {
|
||||
const colonIndex = rk.indexOf(':');
|
||||
const commentId = colonIndex === -1 ? rk : rk.substring(0, colonIndex);
|
||||
if (commentIdsToRemove.includes(commentId)) {
|
||||
delete reactObj[rk];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deleteReacts(myReacts, byId[claimId]);
|
||||
deleteReacts(othersReacts, byId[claimId]);
|
||||
|
||||
delete byId[claimId];
|
||||
delete totalCommentsById[claimId];
|
||||
delete topLevelCommentsById[claimId];
|
||||
delete topLevelTotalCommentsById[claimId];
|
||||
delete topLevelTotalPagesById[claimId];
|
||||
|
||||
return {
|
||||
...state,
|
||||
byId,
|
||||
totalCommentsById,
|
||||
topLevelCommentsById,
|
||||
topLevelTotalCommentsById,
|
||||
topLevelTotalPagesById,
|
||||
myReactsByCommentId: myReacts,
|
||||
othersReactsByCommentId: othersReacts,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_RECEIVED]: (state: CommentsState, action: any) => {
|
||||
const { uri, claimId, comment } = action.data;
|
||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
|
@ -352,21 +546,50 @@ export default handleActions(
|
|||
const { comment_id } = action.data;
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||
|
||||
const comment = commentById[comment_id];
|
||||
|
||||
// to remove the comment and its references
|
||||
const claimId = commentById[comment_id].claim_id;
|
||||
const claimId = comment.claim_id;
|
||||
for (let i = 0; i < byId[claimId].length; i++) {
|
||||
if (byId[claimId][i] === comment_id) {
|
||||
byId[claimId].splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update replies
|
||||
if (comment['parent_id'] && repliesByParentId[comment.parent_id]) {
|
||||
const index = repliesByParentId[comment.parent_id].indexOf(comment.comment_id);
|
||||
if (index > -1) {
|
||||
repliesByParentId[comment.parent_id].splice(index, 1);
|
||||
|
||||
if (commentById[comment.parent_id]) {
|
||||
commentById[comment.parent_id].replies = Math.max(0, (commentById[comment.parent_id].replies || 0) - 1);
|
||||
}
|
||||
|
||||
if (totalRepliesByParentId[comment.parent_id]) {
|
||||
totalRepliesByParentId[comment.parent_id] = Math.max(0, totalRepliesByParentId[comment.parent_id] - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalCommentsById[claimId]) {
|
||||
totalCommentsById[claimId] = Math.max(0, totalCommentsById[claimId] - 1);
|
||||
}
|
||||
|
||||
delete commentById[comment_id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
commentById,
|
||||
byId,
|
||||
totalCommentsById,
|
||||
repliesByParentId,
|
||||
totalRepliesByParentId,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
@ -390,11 +613,41 @@ export default handleActions(
|
|||
isCommenting: false,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
isCmmenting: false,
|
||||
}),
|
||||
|
||||
[ACTIONS.COMMENT_PIN_COMPLETED]: (state: CommentsState, action: any) => {
|
||||
const { pinnedComment, claimId, unpin } = action.data;
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||
|
||||
if (pinnedComment && topLevelCommentsById[claimId]) {
|
||||
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||
if (index > -1) {
|
||||
topLevelCommentsById[claimId].splice(index, 1);
|
||||
|
||||
if (unpin) {
|
||||
// Without the sort score, I have no idea where to put it. Just
|
||||
// dump it at the bottom. Users can refresh if they want it back to
|
||||
// the correct sorted position.
|
||||
topLevelCommentsById[claimId].push(pinnedComment.comment_id);
|
||||
} else {
|
||||
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||
}
|
||||
|
||||
commentById[pinnedComment.comment_id] = pinnedComment;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
commentById,
|
||||
topLevelCommentsById,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
fetchingModerationBlockList: true,
|
||||
|
|
|
@ -9,6 +9,7 @@ const selectState = (state) => state.comments || {};
|
|||
|
||||
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
|
||||
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
|
||||
export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId);
|
||||
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
||||
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
||||
export const selectCommentsDisabledChannelIds = createSelector(
|
||||
|
@ -143,19 +144,28 @@ export const selectCommentsByUri = createSelector(selectState, (state) => {
|
|||
return comments;
|
||||
});
|
||||
|
||||
export const selectLinkedCommentAncestors = createSelector(selectState, (state) => state.linkedCommentAncestors);
|
||||
|
||||
export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
return state.byId[claimId];
|
||||
});
|
||||
|
||||
export const makeSelectMyReactionsForComment = (commentId: string) =>
|
||||
export const selectMyReactionsByCommentId = createSelector(selectState, (state) => state.myReactsByCommentId);
|
||||
|
||||
/**
|
||||
* makeSelectMyReactionsForComment
|
||||
*
|
||||
* @param commentIdChannelId Format = "commentId:MyChannelId".
|
||||
*/
|
||||
export const makeSelectMyReactionsForComment = (commentIdChannelId: string) =>
|
||||
createSelector(selectState, (state) => {
|
||||
if (!state.myReactsByCommentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return state.myReactsByCommentId[commentId] || [];
|
||||
return state.myReactsByCommentId[commentIdChannelId] || [];
|
||||
});
|
||||
|
||||
export const makeSelectOthersReactionsForComment = (commentId: string) =>
|
||||
|
@ -276,6 +286,18 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
|
|||
}
|
||||
);
|
||||
|
||||
export const makeSelectTopLevelTotalCommentsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
return state.topLevelTotalCommentsById[claimId] || 0;
|
||||
});
|
||||
|
||||
export const makeSelectTopLevelTotalPagesForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
return state.topLevelTotalPagesById[claimId] || 0;
|
||||
});
|
||||
|
||||
export const makeSelectRepliesForParentId = (id: string) =>
|
||||
createSelector(
|
||||
selectState, // no selectRepliesByParentId
|
||||
|
@ -335,8 +357,9 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
|||
);
|
||||
|
||||
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
|
||||
createSelector(makeSelectCommentsForUri(uri), (comments) => {
|
||||
return comments ? comments.length : 0;
|
||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
return state.totalCommentsById[claimId] || 0;
|
||||
});
|
||||
|
||||
// Personal list
|
||||
|
|
|
@ -10,11 +10,10 @@ type SortProps = {
|
|||
reactionsById: {},
|
||||
sort: string,
|
||||
isMyComment: (string) => boolean,
|
||||
justCommented: Array<string>,
|
||||
};
|
||||
|
||||
export function sortComments(sortProps: SortProps): Array<Comment> {
|
||||
const { comments, reactionsById, sort, isMyComment, justCommented } = sortProps;
|
||||
const { comments, reactionsById, sort, isMyComment } = sortProps;
|
||||
|
||||
if (!comments) return [];
|
||||
|
||||
|
@ -29,12 +28,10 @@ export function sortComments(sortProps: SortProps): Array<Comment> {
|
|||
|
||||
const aIsMine = isMyComment(a.channel_id);
|
||||
const bIsMine = isMyComment(b.channel_id);
|
||||
const aIsMyRecent = justCommented.includes(a.comment_id);
|
||||
const bIsMyRecent = justCommented.includes(b.comment_id);
|
||||
|
||||
if (aIsMine && justCommented.length && aIsMyRecent) {
|
||||
if (aIsMine) {
|
||||
return -1;
|
||||
} else if (bIsMine && justCommented.length && bIsMyRecent) {
|
||||
} else if (bIsMine) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue