Batch-resolve channels on doCommentList fetch

This commit is contained in:
Rafael 2022-03-02 09:10:52 -03:00 committed by Thomas Zarebczan
parent 7c304702d6
commit 96e7fda26a
7 changed files with 79 additions and 164 deletions

View file

@ -148,7 +148,7 @@ declare type CommentListParams = {
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
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, // @see: ui/constants/comments.js::SORT_BY

View file

@ -1,11 +1,5 @@
import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims';
import {
selectClaimForUri,
makeSelectClaimForUri,
selectClaimIsMine,
selectFetchingMyChannels,
} from 'redux/selectors/claims';
import { selectClaimForUri, selectClaimIsMine, selectFetchingMyChannels } from 'redux/selectors/claims';
import {
selectTopLevelCommentsForUri,
makeSelectTopLevelTotalPagesForUri,
@ -28,25 +22,16 @@ const select = (state, props) => {
const { uri } = props;
const claim = selectClaimForUri(state, uri);
const channelId = getChannelIdFromClaim(claim);
const activeChannelClaim = selectActiveChannelClaim(state);
const topLevelComments = selectTopLevelCommentsForUri(state, uri);
const resolvedComments =
topLevelComments && topLevelComments.length > 0
? topLevelComments.filter(({ channel_url }) => makeSelectClaimForUri(channel_url)(state) !== undefined)
: [];
return {
topLevelComments,
resolvedComments,
topLevelComments: selectTopLevelCommentsForUri(state, uri),
allCommentIds: selectCommentIdsForUri(state, uri),
pinnedComments: selectPinnedCommentsForUri(state, uri),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
claimId: claim && claim.claim_id,
channelId,
channelId: getChannelIdFromClaim(claim),
claimIsMine: selectClaimIsMine(state, claim),
isFetchingComments: selectIsFetchingComments(state),
isFetchingCommentsById: selectIsFetchingCommentsById(state),
@ -64,7 +49,6 @@ const perform = {
fetchComment: doCommentById,
fetchReacts: doCommentReactList,
resetComments: doCommentReset,
doResolveUris,
};
export default connect(select, perform)(CommentsList);

View file

@ -30,7 +30,6 @@ type Props = {
allCommentIds: any,
pinnedComments: Array<Comment>,
topLevelComments: Array<Comment>,
resolvedComments: Array<Comment>,
topLevelTotalPages: number,
uri: string,
claimId?: string,
@ -47,11 +46,10 @@ type Props = {
activeChannelId: ?string,
settingsByChannelId: { [channelId: string]: PerChannelSettings },
commentsAreExpanded?: boolean,
fetchTopLevelComments: (uri: string, parentId: string, page: number, pageSize: number, sortBy: number) => void,
fetchTopLevelComments: (uri: string, parentId: ?string, page: number, pageSize: number, sortBy: number) => void,
fetchComment: (commentId: string) => void,
fetchReacts: (commentIds: Array<string>) => Promise<any>,
resetComments: (claimId: string) => void,
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
};
export default function CommentList(props: Props) {
@ -60,7 +58,6 @@ export default function CommentList(props: Props) {
uri,
pinnedComments,
topLevelComments,
resolvedComments,
topLevelTotalPages,
claimId,
channelId,
@ -79,7 +76,6 @@ export default function CommentList(props: Props) {
fetchComment,
fetchReacts,
resetComments,
doResolveUris,
} = props;
const isMobile = useIsMobile();
@ -89,7 +85,6 @@ export default function CommentList(props: Props) {
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 [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
const [didInitialPageFetch, setInitialPageFetch] = React.useState(false);
const hasDefaultExpansion = commentsAreExpanded || !isMediumScreen || isMobile;
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
@ -97,9 +92,6 @@ export default function CommentList(props: Props) {
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
const moreBelow = page < topLevelTotalPages;
const isResolvingComments = topLevelComments && resolvedComments.length !== topLevelComments.length;
const alreadyResolved = !isResolvingComments && resolvedComments.length !== 0;
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === topLevelComments.length;
const title = getCommentsListTitle(totalComments);
// Display comments immediately if not fetching reactions
@ -123,8 +115,7 @@ export default function CommentList(props: Props) {
}
setPage(1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, uri, resetComments]); // 'claim' is derived from 'uri'
}, [page, claimId, resetComments]);
// Fetch top-level comments
useEffect(() => {
@ -133,7 +124,7 @@ export default function CommentList(props: Props) {
fetchComment(linkedCommentId);
}
fetchTopLevelComments(uri, '', page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
fetchTopLevelComments(uri, undefined, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
}
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
@ -182,6 +173,8 @@ export default function CommentList(props: Props) {
// Infinite scroll
useEffect(() => {
if (topLevelComments.length === 0) return;
function shouldFetchNextPage(page, topLevelTotalPages, yPrefetchPx = 1000) {
if (!spinnerRef || !spinnerRef.current) return false;
@ -216,7 +209,7 @@ export default function CommentList(props: Props) {
setInitialPageFetch(true);
}
if (hasDefaultExpansion && !isFetchingComments && canDisplayComments && readyToDisplayComments && moreBelow) {
if (hasDefaultExpansion && !isFetchingComments && readyToDisplayComments && moreBelow) {
const commentsInDrawer = Boolean(document.querySelector('.MuiDrawer-root .card--enable-overflow'));
const scrollingElement = commentsInDrawer ? document.querySelector('.card--enable-overflow') : window;
@ -227,7 +220,7 @@ export default function CommentList(props: Props) {
}
}
}, [
canDisplayComments,
topLevelComments,
hasDefaultExpansion,
didInitialPageFetch,
isFetchingComments,
@ -238,22 +231,6 @@ export default function CommentList(props: Props) {
topLevelTotalPages,
]);
// Wait to only display topLevelComments after resolved or else
// other components will try to resolve again, like channelThumbnail
useEffect(() => {
if (!isResolvingComments) setCommentsToDisplay(topLevelComments);
}, [isResolvingComments, topLevelComments]);
// Batch resolve comment channel urls
useEffect(() => {
if (!topLevelComments || alreadyResolved) return;
const urisToResolve = [];
topLevelComments.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
if (urisToResolve.length > 0) doResolveUris(urisToResolve, true);
}, [alreadyResolved, doResolveUris, topLevelComments]);
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
@ -282,7 +259,7 @@ export default function CommentList(props: Props) {
<>
{pinnedComments && <CommentElements comments={pinnedComments} {...commentProps} />}
{commentsToDisplay && <CommentElements comments={commentsToDisplay} {...commentProps} />}
<CommentElements comments={topLevelComments} {...commentProps} />
</>
)}
</ul>
@ -308,7 +285,7 @@ export default function CommentList(props: Props) {
</div>
)}
{(isFetchingComments || (hasDefaultExpansion && moreBelow) || !canDisplayComments) && (
{(isFetchingComments || (hasDefaultExpansion && moreBelow)) && (
<div className="main--empty" ref={spinnerRef}>
<Spinner type="small" />
</div>

View file

@ -1,26 +1,16 @@
import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims';
import { selectClaimIsMineForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import { selectClaimIsMineForUri } from 'redux/selectors/claims';
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import CommentsReplies from './view';
const select = (state, props) => {
const fetchedReplies = selectRepliesForParentId(state, props.parentId);
const resolvedReplies =
fetchedReplies && fetchedReplies.length > 0
? fetchedReplies.filter(({ channel_url }) => makeSelectClaimForUri(channel_url)(state) !== undefined)
: [];
const { uri, parentId } = props;
return {
fetchedReplies,
resolvedReplies,
claimIsMine: selectClaimIsMineForUri(state, props.uri),
userCanComment: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
fetchedReplies: selectRepliesForParentId(state, parentId),
claimIsMine: selectClaimIsMineForUri(state, uri),
isFetching: selectIsFetchingCommentsByParentId(state, parentId),
};
};
const perform = (dispatch) => ({ doResolveUris: (uris) => dispatch(doResolveUris(uris, true)) });
export default connect(select, perform)(CommentsReplies);
export default connect(select)(CommentsReplies);

View file

@ -6,61 +6,34 @@ import React from 'react';
import Spinner from 'component/spinner';
type Props = {
fetchedReplies: Array<Comment>,
resolvedReplies: Array<Comment>,
uri: string,
parentId: string,
claimIsMine: boolean,
linkedCommentId?: string,
userCanComment: boolean,
threadDepth: number,
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
isFetchingByParentId: { [string]: boolean },
hasMore: boolean,
supportDisabled: boolean,
doResolveUris: (Array<string>) => void,
onShowMore?: () => void,
// redux
fetchedReplies: Array<Comment>,
claimIsMine: boolean,
isFetching: boolean,
};
function CommentsReplies(props: Props) {
export default function CommentsReplies(props: Props) {
const {
uri,
parentId,
fetchedReplies,
resolvedReplies,
claimIsMine,
linkedCommentId,
userCanComment,
threadDepth,
numDirectReplies,
isFetchingByParentId,
isFetching,
hasMore,
supportDisabled,
doResolveUris,
onShowMore,
} = props;
const [isExpanded, setExpanded] = React.useState(true);
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
// Batch resolve comment channel urls
React.useEffect(() => {
if (!fetchedReplies || alreadyResolved) return;
const urisToResolve = [];
fetchedReplies.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
}, [alreadyResolved, doResolveUris, fetchedReplies]);
// Wait to only display topLevelComments after resolved or else
// other components will try to resolve again, like channelThumbnail
React.useEffect(() => {
if (!isResolvingReplies) setCommentsToDisplay(fetchedReplies);
}, [isResolvingReplies, fetchedReplies]);
return !numDirectReplies ? null : (
<div className="comment__replies-container">
@ -78,24 +51,21 @@ function CommentsReplies(props: Props) {
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
<ul className="comments--replies">
{!isResolvingReplies &&
commentsToDisplay &&
commentsToDisplay.length > 0 &&
commentsToDisplay.map((comment) => (
<Comment
key={comment.comment_id}
threadDepth={threadDepth}
uri={uri}
comment={comment}
claimIsMine={claimIsMine}
linkedCommentId={linkedCommentId}
commentingEnabled={userCanComment}
supportDisabled={supportDisabled}
/>
))}
{fetchedReplies.map((comment) => (
<Comment
key={comment.comment_id}
threadDepth={threadDepth}
uri={uri}
comment={comment}
claimIsMine={claimIsMine}
linkedCommentId={linkedCommentId}
supportDisabled={supportDisabled}
/>
))}
</ul>
</div>
)}
{isExpanded && fetchedReplies && hasMore && (
<div className="comment__actions--nested">
<Button
@ -106,7 +76,8 @@ function CommentsReplies(props: Props) {
/>
</div>
)}
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
{isFetching && (
<div className="comment__replies-container">
<div className="comment__actions--nested">
<Spinner type="small" />
@ -116,5 +87,3 @@ function CommentsReplies(props: Props) {
</div>
);
}
export default CommentsReplies;

View file

@ -19,6 +19,7 @@ import {
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { toHex } from 'util/hex';
import { getChannelFromClaim } from 'util/claim';
import Comments from 'comments';
import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app';
@ -30,47 +31,41 @@ const MENTION_REGEX = /(?:^| |\n)@[^\s=&#$@%?:;/"<>%{}|^~[]*(?::[\w]+)?/gm;
export function doCommentList(
uri: string,
parentId: 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];
const claimId = claim ? claim.claim_id : null;
const claim = selectClaimForUri(state, uri);
const { claim_id: claimId } = claim || {};
if (!claimId) {
dispatch({
type: ACTIONS.COMMENT_LIST_FAILED,
data: 'unable to find claim for uri',
});
return;
return dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: 'unable to find claim for uri' });
}
dispatch({
type: ACTIONS.COMMENT_LIST_STARTED,
data: {
parentId,
},
});
dispatch({ type: ACTIONS.COMMENT_LIST_STARTED, data: { parentId } });
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
const creatorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
const creatorChannelClaim = getChannelFromClaim(claim);
const { claim_id: creatorClaimId, name: channelName } = creatorChannelClaim || {};
return Comments.comment_list({
page,
claim_id: claimId,
page_size: pageSize,
parent_id: parentId || undefined,
parent_id: parentId,
top_level: !parentId,
channel_id: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
channel_name: creatorChannelClaim ? creatorChannelClaim.name : undefined,
channel_id: creatorClaimId,
channel_name: channelName,
sort_by: sortBy,
})
.then((result: CommentListResponse) => {
const { items: comments, total_items, total_filtered_items, total_pages } = result;
dispatch({
const commentChannelUrls = comments && comments.map((comment) => comment.channel_url || '');
const dispatchData = {
type: ACTIONS.COMMENT_LIST_COMPLETED,
data: {
comments,
@ -78,38 +73,36 @@ export function doCommentList(
totalItems: total_items,
totalFilteredItems: total_filtered_items,
totalPages: total_pages,
claimId: claimId,
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
uri: uri,
claimId,
creatorClaimId,
uri,
},
});
};
return result;
// Batch resolve comment channel urls
if (commentChannelUrls) {
return dispatch(async () => await doResolveUris(commentChannelUrls, true)).then(() => {
dispatch({ ...dispatchData });
return result;
});
} else {
dispatch({ ...dispatchData });
return result;
}
})
.catch((error) => {
switch (error.message) {
const { message } = error;
switch (message) {
case 'comments are disabled by the creator':
dispatch({
type: ACTIONS.COMMENT_LIST_COMPLETED,
data: {
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
disabled: true,
},
});
break;
return dispatch({ type: ACTIONS.COMMENT_LIST_COMPLETED, data: { creatorClaimId, disabled: true } });
case FETCH_API_FAILED_TO_FETCH:
dispatch(
doToast({
isError: true,
message: __('Failed to fetch comments.'),
})
);
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
break;
dispatch(doToast({ isError: true, message: __('Failed to fetch comments.') }));
return dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
default:
dispatch(doToast({ isError: true, message: `${error.message}` }));
dispatch(doToast({ isError: true, message: `${message}` }));
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
}
});

View file

@ -24,7 +24,6 @@ export const selectCommentsById = (state: State) => selectState(state).commentBy
export const selectCommentIdsByClaimId = (state: State) => selectState(state).byId;
export const selectIsFetchingComments = (state: State) => selectState(state).isLoading;
export const selectIsFetchingCommentsById = (state: State) => selectState(state).isLoadingById;
export const selectIsFetchingCommentsByParentId = (state: State) => selectState(state).isLoadingByParentId;
export const selectIsFetchingReacts = (state: State) => selectState(state).isFetchingReacts;
export const selectMyReacts = (state: State) => state.comments.myReactsByCommentId;
@ -32,6 +31,9 @@ export const selectMyReactsForComment = (state: State, commentIdChannelId: strin
// @commentIdChannelId: Format = 'commentId:MyChannelId'
return state.comments.myReactsByCommentId && state.comments.myReactsByCommentId[commentIdChannelId];
};
export const selectIsFetchingCommentsByParentId = (state: State, parentId: string) => {
return selectState(state).isLoadingByParentId[parentId];
};
export const selectOthersReacts = (state: State) => state.comments.othersReactsByCommentId;
export const selectOthersReactsForComment = (state: State, id: string) => {