Re-design comment threads (#1489)
* Redesign threadline and fetching state - threadline goes right below channel avatar, mimicking reddits implementation, has a increase effect on hover and is slimmer, creating more space for comments on screen - fetching state now replaces show/hide button, also mimicking reddit, and now says that it is loading, instead of a blank spinner, and also improves space a bit * Redesign comment threads - Allow for infinite comment chains - Can go back and forth between the pages - Can go back to all comments or to the first comment in the chain - Some other improvements, which include: - add title on non-drawer comment sections (couldn't see amount of comments) - fix Expandable component (would begin expanded and collapse after the effect runs, which looked bad and shifted the layout, now each comments greater than the set length begins collapsed) - used constants for consistency * Fix replying to last thread comment * Fix buttons condition (only on fetched comment to avoid deleted case) * Fix auto-scroll * Bring back instant feedback for Show More replies * Improve thread back links - Now going back to all comments links the top-level comment for easier navigation - Going back to ~ previous ~ now goes back into the chain instead of topmost level * Clear timeouts due to unrelated issue * Fix deep thread linked comment case and more scroll improvements * More minor changes * Flow * Fix commentList tile style * Fix long channel names overflowing on small screens * More scroll changes * Fix threadline * Revert "Fix long channel names overflowing on small screens" This reverts commit e4d2dc7da5861ed8136a60f3352e41a690cd4d33. * Fix replies fetch * Revert "Fix replies fetch" This reverts commit ec70054675a604a7a5f3764ba07c36bf7b0f49c8. * Cleanup and make smooth * Always use linked comment on threads * Cleanup * Higlight thread comment * Fix comment body styles
This commit is contained in:
parent
f6f15531d4
commit
b75a4014b6
25 changed files with 507 additions and 260 deletions
2
flow-typed/Comment.js
vendored
2
flow-typed/Comment.js
vendored
|
@ -52,7 +52,7 @@ declare type CommentsState = {
|
|||
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 }, // commentId -> Comment
|
||||
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||
fetchedCommentAncestors: { [string]: Array<string> }, // {"fetchedCommentId": ["parentId", "grandParentId", ...]}
|
||||
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
|
||||
isLoading: boolean,
|
||||
isLoadingById: boolean,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import { LINKED_COMMENT_QUERY_PARAM, THREAD_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
import ChannelDiscussion from './view';
|
||||
import { makeSelectTagInClaimOrChannelForUri, selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||
|
@ -16,7 +17,8 @@ const select = (state, props) => {
|
|||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
|
||||
return {
|
||||
linkedCommentId: urlParams.get('lc'),
|
||||
linkedCommentId: urlParams.get(LINKED_COMMENT_QUERY_PARAM),
|
||||
threadCommentId: urlParams.get(THREAD_COMMENT_QUERY_PARAM),
|
||||
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||
commentSettingDisabled: channelSettings && !channelSettings.comments_enabled,
|
||||
};
|
||||
|
|
|
@ -8,12 +8,13 @@ const CommentsList = lazyImport(() => import('component/commentsList' /* webpack
|
|||
type Props = {
|
||||
uri: string,
|
||||
linkedCommentId?: string,
|
||||
threadCommentId?: string,
|
||||
commentsDisabled: boolean,
|
||||
commentSettingDisabled?: boolean,
|
||||
};
|
||||
|
||||
function ChannelDiscussion(props: Props) {
|
||||
const { uri, linkedCommentId, commentsDisabled, commentSettingDisabled } = props;
|
||||
const { uri, linkedCommentId, threadCommentId, commentsDisabled, commentSettingDisabled } = props;
|
||||
|
||||
if (commentsDisabled) {
|
||||
return <Empty text={__('The creator of this content has disabled comments.')} />;
|
||||
|
@ -26,7 +27,13 @@ function ChannelDiscussion(props: Props) {
|
|||
return (
|
||||
<section className="section">
|
||||
<React.Suspense fallback={null}>
|
||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />
|
||||
<CommentsList
|
||||
uri={uri}
|
||||
linkedCommentId={linkedCommentId}
|
||||
threadCommentId={threadCommentId}
|
||||
commentsAreExpanded
|
||||
notInDrawer
|
||||
/>
|
||||
</React.Suspense>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -12,15 +12,15 @@ import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
|||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doClearPlayingUri } from 'redux/actions/content';
|
||||
import {
|
||||
selectLinkedCommentAncestors,
|
||||
selectFetchedCommentAncestors,
|
||||
selectOthersReactsForComment,
|
||||
makeSelectTotalReplyPagesForParentId,
|
||||
selectIsFetchingCommentsForParentId,
|
||||
selectRepliesForParentId,
|
||||
} from 'redux/selectors/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import {
|
||||
selectUserVerifiedEmail,
|
||||
} from 'redux/selectors/user';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import Comment from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -42,9 +42,11 @@ const select = (state, props) => {
|
|||
hasChannels: selectHasChannels(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
stakedLevel: selectStakedLevelForChannelUri(state, channel_url),
|
||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||
linkedCommentAncestors: selectFetchedCommentAncestors(state),
|
||||
totalReplyPages: makeSelectTotalReplyPagesForParentId(comment_id)(state),
|
||||
selectOdyseeMembershipForUri: channel_url && selectOdyseeMembershipForUri(state, channel_url),
|
||||
repliesFetching: selectIsFetchingCommentsForParentId(state, comment_id),
|
||||
fetchedReplies: selectRepliesForParentId(state, comment_id),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,12 @@ import * as ICONS from 'constants/icons';
|
|||
import * as PAGES from 'constants/pages';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import { COMMENT_HIGHLIGHTED } from 'constants/classnames';
|
||||
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
|
||||
import {
|
||||
SORT_BY,
|
||||
COMMENT_PAGE_SIZE_REPLIES,
|
||||
LINKED_COMMENT_QUERY_PARAM,
|
||||
THREAD_COMMENT_QUERY_PARAM,
|
||||
} 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';
|
||||
|
@ -31,6 +36,7 @@ import { getChannelFromClaim } from 'util/claim';
|
|||
import { parseSticker } from 'util/comments';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||
|
||||
|
@ -47,12 +53,12 @@ type Props = {
|
|||
totalReplyPages: number,
|
||||
commentModBlock: (string) => void,
|
||||
linkedCommentId?: string,
|
||||
threadCommentId?: string,
|
||||
linkedCommentAncestors: { [string]: Array<string> },
|
||||
hasChannels: boolean,
|
||||
commentingEnabled: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
isTopLevel?: boolean,
|
||||
threadDepth: number,
|
||||
hideActions?: boolean,
|
||||
othersReacts: ?{
|
||||
like: number,
|
||||
|
@ -66,6 +72,10 @@ type Props = {
|
|||
setQuickReply: (any) => void,
|
||||
quickReply: any,
|
||||
selectOdyseeMembershipForUri: string,
|
||||
fetchedReplies: Array<Comment>,
|
||||
repliesFetching: boolean,
|
||||
threadLevel?: number,
|
||||
threadDepthLevel?: number,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -82,12 +92,12 @@ function CommentView(props: Props) {
|
|||
fetchReplies,
|
||||
totalReplyPages,
|
||||
linkedCommentId,
|
||||
threadCommentId,
|
||||
linkedCommentAncestors,
|
||||
commentingEnabled,
|
||||
hasChannels,
|
||||
doToast,
|
||||
isTopLevel,
|
||||
threadDepth,
|
||||
hideActions,
|
||||
othersReacts,
|
||||
playingUri,
|
||||
|
@ -96,6 +106,10 @@ function CommentView(props: Props) {
|
|||
setQuickReply,
|
||||
quickReply,
|
||||
selectOdyseeMembershipForUri,
|
||||
fetchedReplies,
|
||||
repliesFetching,
|
||||
threadLevel = 0,
|
||||
threadDepthLevel = 0,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -117,6 +131,11 @@ function CommentView(props: Props) {
|
|||
const commentIsMine = channelId && myChannelIds && myChannelIds.includes(channelId);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const ROUGH_HEADER_HEIGHT = isMobile ? 56 : 60; // @see: --header-height
|
||||
|
||||
const lastThreadLevel = threadDepthLevel - 1;
|
||||
// Mobile: 0, 1, 2 -> new thread....., so each 3 comments
|
||||
const openNewThread = threadLevel > 0 && threadLevel % lastThreadLevel === 0;
|
||||
|
||||
const {
|
||||
push,
|
||||
|
@ -124,12 +143,14 @@ function CommentView(props: Props) {
|
|||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const isLinkedComment = linkedCommentId && linkedCommentId === commentId;
|
||||
const isThreadComment = threadCommentId && threadCommentId === commentId;
|
||||
const isInLinkedCommentChain =
|
||||
linkedCommentId &&
|
||||
linkedCommentAncestors[linkedCommentId] &&
|
||||
linkedCommentAncestors[linkedCommentId].includes(commentId);
|
||||
const showRepliesOnMount = isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES;
|
||||
const showRepliesOnMount = isThreadComment || isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES;
|
||||
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
|
@ -146,6 +167,7 @@ function CommentView(props: Props) {
|
|||
const contentChannelClaim = getChannelFromClaim(claim);
|
||||
const commentByOwnerOfContent = contentChannelClaim && contentChannelClaim.permanent_url === authorUri;
|
||||
const stickerFromMessage = parseSticker(message);
|
||||
const isExpandable = editedMessage.length >= LENGTH_TO_COLLAPSE;
|
||||
|
||||
let channelOwnerOfContent;
|
||||
try {
|
||||
|
@ -208,37 +230,36 @@ function CommentView(props: Props) {
|
|||
}
|
||||
|
||||
function handleTimeClick() {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
urlParams.delete('lc');
|
||||
urlParams.append('lc', commentId);
|
||||
urlParams.set(LINKED_COMMENT_QUERY_PARAM, commentId);
|
||||
replace(`${pathname}?${urlParams.toString()}`);
|
||||
}
|
||||
|
||||
function handleOpenNewThread() {
|
||||
urlParams.set(LINKED_COMMENT_QUERY_PARAM, commentId);
|
||||
urlParams.set(THREAD_COMMENT_QUERY_PARAM, commentId);
|
||||
push({ pathname, search: urlParams.toString() });
|
||||
}
|
||||
|
||||
const linkedCommentRef = React.useCallback(
|
||||
(node) => {
|
||||
if (node !== null && window.pendingLinkedCommentScroll) {
|
||||
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
||||
delete window.pendingLinkedCommentScroll;
|
||||
|
||||
const mobileChatElem = document.querySelector('.MuiPaper-root .card--enable-overflow');
|
||||
const drawerElem = document.querySelector('.MuiDrawer-root');
|
||||
const elem = (isMobile && mobileChatElem) || window;
|
||||
|
||||
if (elem) {
|
||||
// $FlowFixMe
|
||||
elem.scrollTo({
|
||||
top:
|
||||
node.getBoundingClientRect().top +
|
||||
// $FlowFixMe
|
||||
(mobileChatElem && drawerElem ? drawerElem.getBoundingClientRect().top * -1 : elem.scrollY) -
|
||||
ROUGH_HEADER_HEIGHT,
|
||||
// $FlowFixMe
|
||||
top: node.getBoundingClientRect().top + (mobileChatElem ? 0 : elem.scrollY) - ROUGH_HEADER_HEIGHT,
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[isMobile]
|
||||
[ROUGH_HEADER_HEIGHT, isMobile]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -250,28 +271,31 @@ function CommentView(props: Props) {
|
|||
})}
|
||||
id={commentId}
|
||||
>
|
||||
<div
|
||||
ref={isLinkedComment ? linkedCommentRef : undefined}
|
||||
className={classnames('comment__content', {
|
||||
[COMMENT_HIGHLIGHTED]: isLinkedComment,
|
||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail
|
||||
uri={authorUri}
|
||||
obscure={channelIsBlocked}
|
||||
xsmall
|
||||
className="comment__author-thumbnail"
|
||||
checkMembership={false}
|
||||
/>
|
||||
) : (
|
||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" checkMembership={false} />
|
||||
)}
|
||||
</div>
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail
|
||||
uri={authorUri}
|
||||
obscure={channelIsBlocked}
|
||||
xsmall
|
||||
className="comment__author-thumbnail"
|
||||
checkMembership={false}
|
||||
/>
|
||||
) : (
|
||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" checkMembership={false} />
|
||||
)}
|
||||
|
||||
<div className="comment__body-container">
|
||||
{numDirectReplies > 0 && showReplies && (
|
||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setShowReplies(false)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="comment__content" ref={isLinkedComment || isThreadComment ? linkedCommentRef : undefined}>
|
||||
<div
|
||||
className={classnames('comment__body-container', {
|
||||
[COMMENT_HIGHLIGHTED]: isLinkedComment || (isThreadComment && !linkedCommentId),
|
||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{!author ? (
|
||||
|
@ -361,8 +385,8 @@ function CommentView(props: Props) {
|
|||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
) : isExpandable ? (
|
||||
<Expandable beginCollapsed>
|
||||
<MarkdownPreview
|
||||
content={message}
|
||||
promptLinks
|
||||
|
@ -384,49 +408,60 @@ function CommentView(props: Props) {
|
|||
|
||||
{!hideActions && (
|
||||
<div className="comment__actions">
|
||||
{threadDepth !== 0 && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
iconSize={isMobile && 12}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
iconSize={isMobile && 12}
|
||||
/>
|
||||
{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);
|
||||
}
|
||||
}}
|
||||
icon={ICONS.DOWN}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{numDirectReplies > 0 && showReplies && (
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
label={__('Hide replies')}
|
||||
button="link"
|
||||
onClick={() => setShowReplies(false)}
|
||||
icon={ICONS.UP}
|
||||
/>
|
||||
</div>
|
||||
{repliesFetching && (!fetchedReplies || fetchedReplies.length === 0) ? (
|
||||
<span className="comment__actions comment__replies-loading">
|
||||
<Spinner text={numDirectReplies > 1 ? __('Loading Replies') : __('Loading Reply')} type="small" />
|
||||
</span>
|
||||
) : (
|
||||
numDirectReplies > 0 && (
|
||||
<div className="comment__actions">
|
||||
{!showReplies ? (
|
||||
openNewThread ? (
|
||||
<Button
|
||||
label={__('Continue Thread')}
|
||||
button="link"
|
||||
onClick={handleOpenNewThread}
|
||||
iconRight={ICONS.ARROW_RIGHT}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
label={
|
||||
numDirectReplies < 2
|
||||
? __('Show reply')
|
||||
: __('Show %count% replies', { count: numDirectReplies })
|
||||
}
|
||||
button="link"
|
||||
onClick={() => {
|
||||
setShowReplies(true);
|
||||
if (page === 0) {
|
||||
setPage(1);
|
||||
}
|
||||
}}
|
||||
iconRight={ICONS.DOWN}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
label={__('Hide replies')}
|
||||
button="link"
|
||||
onClick={() => setShowReplies(false)}
|
||||
iconRight={ICONS.UP}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{isReplying && (
|
||||
|
@ -435,7 +470,11 @@ function CommentView(props: Props) {
|
|||
uri={uri}
|
||||
parentId={commentId}
|
||||
onDoneReplying={() => {
|
||||
setShowReplies(true);
|
||||
if (openNewThread) {
|
||||
handleOpenNewThread();
|
||||
} else {
|
||||
setShowReplies(true);
|
||||
}
|
||||
setReplying(false);
|
||||
}}
|
||||
onCancelReplying={() => {
|
||||
|
@ -448,19 +487,21 @@ function CommentView(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showReplies && (
|
||||
<CommentsReplies
|
||||
threadDepth={threadDepth - 1}
|
||||
uri={uri}
|
||||
parentId={commentId}
|
||||
linkedCommentId={linkedCommentId}
|
||||
numDirectReplies={numDirectReplies}
|
||||
onShowMore={() => setPage(page + 1)}
|
||||
hasMore={page < totalReplyPages}
|
||||
/>
|
||||
)}
|
||||
{showReplies && (
|
||||
<CommentsReplies
|
||||
threadLevel={threadLevel}
|
||||
uri={uri}
|
||||
parentId={commentId}
|
||||
linkedCommentId={linkedCommentId}
|
||||
threadCommentId={threadCommentId}
|
||||
numDirectReplies={numDirectReplies}
|
||||
onShowMore={() => setPage(page + 1)}
|
||||
hasMore={page < totalReplyPages}
|
||||
threadDepthLevel={threadDepthLevel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import { getChannelFromClaim } from 'util/claim';
|
||||
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
import { MenuList, MenuItem } from '@reach/menu-button';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { URL } from 'config';
|
||||
|
@ -163,8 +164,8 @@ function CommentMenuList(props: Props) {
|
|||
|
||||
function handleCopyCommentLink() {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
urlParams.delete('lc');
|
||||
urlParams.append('lc', commentId);
|
||||
urlParams.delete(LINKED_COMMENT_QUERY_PARAM);
|
||||
urlParams.append(LINKED_COMMENT_QUERY_PARAM, commentId);
|
||||
navigator.clipboard
|
||||
.writeText(`${URL}${pathname}?${urlParams.toString()}`)
|
||||
.then(() => doToast({ message: __('Link copied.') }));
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
selectCommentIdsForUri,
|
||||
selectSettingsByChannelId,
|
||||
selectPinnedCommentsForUri,
|
||||
selectCommentForCommentId,
|
||||
selectCommentAncestorsForId,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
|
@ -25,13 +27,15 @@ import { doFetchUserMemberships } from 'redux/actions/user';
|
|||
import CommentsList from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri } = props;
|
||||
const { uri, threadCommentId, linkedCommentId } = props;
|
||||
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
const threadComment = selectCommentForCommentId(state, threadCommentId);
|
||||
|
||||
return {
|
||||
topLevelComments: selectTopLevelCommentsForUri(state, uri),
|
||||
topLevelComments: threadComment ? [threadComment] : selectTopLevelCommentsForUri(state, uri),
|
||||
threadComment,
|
||||
allCommentIds: selectCommentIdsForUri(state, uri),
|
||||
pinnedComments: selectPinnedCommentsForUri(state, uri),
|
||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||
|
@ -48,6 +52,8 @@ const select = (state, props) => {
|
|||
othersReactsById: selectOthersReacts(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
threadCommentAncestors: selectCommentAncestorsForId(state, threadCommentId),
|
||||
linkedCommentAncestors: selectCommentAncestorsForId(state, linkedCommentId),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
// @flow
|
||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||
import {
|
||||
COMMENT_PAGE_SIZE_TOP_LEVEL,
|
||||
SORT_BY,
|
||||
LINKED_COMMENT_QUERY_PARAM,
|
||||
THREAD_COMMENT_QUERY_PARAM,
|
||||
} from 'constants/comment';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import { getCommentsListTitle } from 'util/comments';
|
||||
|
@ -16,6 +21,7 @@ import React, { useEffect } from 'react';
|
|||
import Spinner from 'component/spinner';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -47,6 +53,11 @@ type Props = {
|
|||
activeChannelId: ?string,
|
||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||
commentsAreExpanded?: boolean,
|
||||
threadCommentId: ?string,
|
||||
threadComment: ?Comment,
|
||||
notInDrawer?: boolean,
|
||||
threadCommentAncestors: ?Array<string>,
|
||||
linkedCommentAncestors: ?Array<string>,
|
||||
fetchTopLevelComments: (uri: string, parentId: ?string, page: number, pageSize: number, sortBy: number) => void,
|
||||
fetchComment: (commentId: string) => void,
|
||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||
|
@ -75,6 +86,11 @@ export default function CommentList(props: Props) {
|
|||
activeChannelId,
|
||||
settingsByChannelId,
|
||||
commentsAreExpanded,
|
||||
threadCommentId,
|
||||
threadComment,
|
||||
notInDrawer,
|
||||
threadCommentAncestors,
|
||||
linkedCommentAncestors,
|
||||
fetchTopLevelComments,
|
||||
fetchComment,
|
||||
fetchReacts,
|
||||
|
@ -83,6 +99,13 @@ export default function CommentList(props: Props) {
|
|||
doFetchUserMemberships,
|
||||
} = props;
|
||||
|
||||
const threadRedirect = React.useRef(false);
|
||||
|
||||
const {
|
||||
push,
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
|
||||
|
@ -99,6 +122,16 @@ export default function CommentList(props: Props) {
|
|||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const moreBelow = page < topLevelTotalPages;
|
||||
const title = getCommentsListTitle(totalComments);
|
||||
const threadDepthLevel = isMobile ? 3 : 10;
|
||||
let threadCommentParent;
|
||||
if (threadCommentAncestors) {
|
||||
threadCommentAncestors.some((ancestor, index) => {
|
||||
if (index >= threadDepthLevel - 1) return true;
|
||||
|
||||
threadCommentParent = ancestor;
|
||||
});
|
||||
}
|
||||
const threadTopLevelComment = threadCommentAncestors && threadCommentAncestors[threadCommentAncestors.length - 1];
|
||||
|
||||
// Display comments immediately if not fetching reactions
|
||||
// If not, wait to show comments until reactions are fetched
|
||||
|
@ -125,13 +158,37 @@ export default function CommentList(props: Props) {
|
|||
setPage(1);
|
||||
}, [claimId, resetComments]);
|
||||
|
||||
function refreshComments() {
|
||||
// Invalidate existing comments
|
||||
setPage(0);
|
||||
}
|
||||
|
||||
function changeSort(newSort) {
|
||||
if (sort !== newSort) {
|
||||
setSort(newSort);
|
||||
setPage(0); // Invalidate existing comments
|
||||
refreshComments();
|
||||
}
|
||||
}
|
||||
|
||||
// If a linked comment is deep within a thread, redirect to it's own thread page
|
||||
// based on the set depthLevel (mobile/desktop)
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!threadCommentId &&
|
||||
linkedCommentId &&
|
||||
linkedCommentAncestors &&
|
||||
linkedCommentAncestors.length > threadDepthLevel - 1 &&
|
||||
!threadRedirect.current
|
||||
) {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
urlParams.set(THREAD_COMMENT_QUERY_PARAM, linkedCommentId);
|
||||
|
||||
push({ pathname, search: urlParams.toString() });
|
||||
// to do it only once
|
||||
threadRedirect.current = true;
|
||||
}
|
||||
}, [linkedCommentAncestors, linkedCommentId, pathname, push, search, threadCommentId, threadDepthLevel]);
|
||||
|
||||
// Force comments reset
|
||||
useEffect(() => {
|
||||
if (page === 0) {
|
||||
|
@ -147,8 +204,13 @@ export default function CommentList(props: Props) {
|
|||
// Fetch top-level comments
|
||||
useEffect(() => {
|
||||
if (page !== 0) {
|
||||
if (page === 1 && linkedCommentId) {
|
||||
fetchComment(linkedCommentId);
|
||||
if (page === 1) {
|
||||
if (threadCommentId) {
|
||||
fetchComment(threadCommentId);
|
||||
}
|
||||
if (linkedCommentId) {
|
||||
fetchComment(linkedCommentId);
|
||||
}
|
||||
}
|
||||
|
||||
fetchTopLevelComments(uri, undefined, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
||||
|
@ -157,7 +219,13 @@ export default function CommentList(props: Props) {
|
|||
// no need to listen for uri change, claimId change will trigger page which
|
||||
// will handle this
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort]);
|
||||
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, threadCommentId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (threadCommentId) {
|
||||
refreshComments();
|
||||
}
|
||||
}, [threadCommentId]);
|
||||
|
||||
// Fetch reacts
|
||||
useEffect(() => {
|
||||
|
@ -194,7 +262,7 @@ export default function CommentList(props: Props) {
|
|||
|
||||
// Scroll to linked-comment
|
||||
useEffect(() => {
|
||||
if (linkedCommentId) {
|
||||
if (linkedCommentId || threadCommentId) {
|
||||
window.pendingLinkedCommentScroll = true;
|
||||
} else {
|
||||
delete window.pendingLinkedCommentScroll;
|
||||
|
@ -262,30 +330,58 @@ export default function CommentList(props: Props) {
|
|||
topLevelTotalPages,
|
||||
]);
|
||||
|
||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||
const commentProps = {
|
||||
isTopLevel: true,
|
||||
uri,
|
||||
claimIsMine,
|
||||
linkedCommentId,
|
||||
threadCommentId,
|
||||
threadDepthLevel,
|
||||
};
|
||||
const actionButtonsProps = {
|
||||
totalComments,
|
||||
sort,
|
||||
changeSort,
|
||||
setPage,
|
||||
handleRefresh: () => setPage(0),
|
||||
handleRefresh: refreshComments,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="card--enable-overflow"
|
||||
title={!isMobile && title}
|
||||
className="card--enable-overflow comment__list"
|
||||
title={(!isMobile || notInDrawer) && title}
|
||||
titleActions={<CommentActionButtons {...actionButtonsProps} />}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && <CommentActionButtons {...actionButtonsProps} />}
|
||||
{isMobile && !notInDrawer && <CommentActionButtons {...actionButtonsProps} />}
|
||||
|
||||
<CommentCreate uri={uri} />
|
||||
|
||||
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
||||
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
||||
{threadCommentId && threadComment && (
|
||||
<span className="comment__actions comment__thread-links">
|
||||
<ThreadLinkButton
|
||||
label={__('View all comments')}
|
||||
threadCommentParent={threadTopLevelComment || threadCommentId}
|
||||
threadCommentId={threadCommentId}
|
||||
isViewAll
|
||||
/>
|
||||
|
||||
{threadCommentParent && (
|
||||
<ThreadLinkButton
|
||||
label={__('Show parent comments')}
|
||||
threadCommentParent={threadCommentParent}
|
||||
threadCommentId={threadCommentId}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{channelSettings &&
|
||||
channelSettings.comments_enabled &&
|
||||
!isFetchingComments &&
|
||||
!totalComments &&
|
||||
!threadCommentId && <Empty padded text={__('That was pretty deep. What do you think?')} />}
|
||||
|
||||
<ul
|
||||
ref={commentListRef}
|
||||
className={classnames('comments', {
|
||||
|
@ -294,7 +390,7 @@ export default function CommentList(props: Props) {
|
|||
>
|
||||
{readyToDisplayComments && (
|
||||
<>
|
||||
{pinnedComments && <CommentElements comments={pinnedComments} {...commentProps} />}
|
||||
{pinnedComments && !threadCommentId && <CommentElements comments={pinnedComments} {...commentProps} />}
|
||||
<CommentElements comments={topLevelComments} {...commentProps} />
|
||||
</>
|
||||
)}
|
||||
|
@ -331,7 +427,7 @@ export default function CommentList(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{(isFetchingComments || (hasDefaultExpansion && moreBelow)) && (
|
||||
{(threadCommentId ? !readyToDisplayComments : isFetchingComments || (hasDefaultExpansion && moreBelow)) && (
|
||||
<div className="main--empty" ref={spinnerRef}>
|
||||
<Spinner type="small" />
|
||||
</div>
|
||||
|
@ -403,3 +499,45 @@ const SortButton = (sortButtonProps: SortButtonProps) => {
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ThreadLinkProps = {
|
||||
label: string,
|
||||
isViewAll?: boolean,
|
||||
threadCommentParent: string,
|
||||
threadCommentId: string,
|
||||
};
|
||||
|
||||
const ThreadLinkButton = (props: ThreadLinkProps) => {
|
||||
const { label, isViewAll, threadCommentParent, threadCommentId } = props;
|
||||
|
||||
const {
|
||||
push,
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
||||
return (
|
||||
<Button
|
||||
button="link"
|
||||
label={label}
|
||||
icon={ICONS.ARROW_LEFT}
|
||||
iconSize={12}
|
||||
onClick={() => {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
||||
if (!isViewAll) {
|
||||
urlParams.set(THREAD_COMMENT_QUERY_PARAM, threadCommentParent);
|
||||
// on moving back, link the current thread comment so that it auto-expands into the correct conversation
|
||||
urlParams.set(LINKED_COMMENT_QUERY_PARAM, threadCommentId);
|
||||
} else {
|
||||
urlParams.delete(THREAD_COMMENT_QUERY_PARAM);
|
||||
// links the top-level comment when going back to all comments, for easy locating
|
||||
// in the middle of big comment sections
|
||||
urlParams.set(LINKED_COMMENT_QUERY_PARAM, threadCommentParent);
|
||||
}
|
||||
window.pendingLinkedCommentScroll = true;
|
||||
|
||||
push({ pathname, search: urlParams.toString() });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import Comment from 'component/comment';
|
||||
import React from 'react';
|
||||
|
@ -8,14 +7,16 @@ import Spinner from 'component/spinner';
|
|||
type Props = {
|
||||
uri: string,
|
||||
linkedCommentId?: string,
|
||||
threadDepth: number,
|
||||
threadCommentId?: string,
|
||||
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
||||
hasMore: boolean,
|
||||
supportDisabled: boolean,
|
||||
threadDepthLevel?: number,
|
||||
onShowMore?: () => void,
|
||||
// redux
|
||||
fetchedReplies: Array<Comment>,
|
||||
claimIsMine: boolean,
|
||||
threadLevel: number,
|
||||
isFetching: boolean,
|
||||
};
|
||||
|
||||
|
@ -25,67 +26,50 @@ export default function CommentsReplies(props: Props) {
|
|||
fetchedReplies,
|
||||
claimIsMine,
|
||||
linkedCommentId,
|
||||
threadDepth,
|
||||
threadCommentId,
|
||||
numDirectReplies,
|
||||
isFetching,
|
||||
hasMore,
|
||||
supportDisabled,
|
||||
threadDepthLevel,
|
||||
onShowMore,
|
||||
threadLevel,
|
||||
isFetching,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setExpanded] = React.useState(true);
|
||||
|
||||
return !numDirectReplies ? null : (
|
||||
<div className="comment__replies-container">
|
||||
{!isExpanded ? (
|
||||
<div className="comment__actions--nested">
|
||||
<Button
|
||||
className="comment__action"
|
||||
label={__('Show Replies')}
|
||||
onClick={() => setExpanded(!isExpanded)}
|
||||
icon={isExpanded ? ICONS.UP : ICONS.DOWN}
|
||||
<ul className="comment__replies">
|
||||
{fetchedReplies.map((comment) => (
|
||||
<Comment
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
comment={comment}
|
||||
claimIsMine={claimIsMine}
|
||||
linkedCommentId={linkedCommentId}
|
||||
threadCommentId={threadCommentId}
|
||||
supportDisabled={supportDisabled}
|
||||
threadLevel={threadLevel + 1}
|
||||
threadDepthLevel={threadDepthLevel}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="comment__replies">
|
||||
{fetchedReplies.length > 0 && (
|
||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||
)}
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className="comments--replies">
|
||||
{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
|
||||
button="link"
|
||||
label={__('Show more')}
|
||||
onClick={() => onShowMore && onShowMore()}
|
||||
className="button--uri-indicator"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFetching && (
|
||||
<div className="comment__replies-container">
|
||||
{fetchedReplies.length > 0 &&
|
||||
hasMore &&
|
||||
(isFetching ? (
|
||||
<span className="comment__actions--nested comment__replies-loading--more">
|
||||
<Spinner text={__('Loading')} type="small" />
|
||||
</span>
|
||||
) : (
|
||||
<div className="comment__actions--nested">
|
||||
<Spinner type="small" />
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Show more')}
|
||||
onClick={() => onShowMore && onShowMore()}
|
||||
className="button--uri-indicator"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,11 +53,13 @@ export default function WaitUntilOnPage(props: Props) {
|
|||
|
||||
// Handles "element is already in viewport when mounted".
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!shouldRender && shouldElementRender(ref)) {
|
||||
setShouldRender(true);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Handles "element scrolled into viewport".
|
||||
|
|
|
@ -8,12 +8,14 @@ const COLLAPSED_HEIGHT = 120;
|
|||
|
||||
type Props = {
|
||||
children: React$Node | Array<React$Node>,
|
||||
beginCollapsed?: boolean,
|
||||
};
|
||||
|
||||
export default function Expandable(props: Props) {
|
||||
const { children, beginCollapsed } = props;
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [rect, setRect] = useState();
|
||||
const { children } = props;
|
||||
const ref = useRef();
|
||||
|
||||
// Update the rect initially & when the window size changes.
|
||||
|
@ -40,7 +42,7 @@ export default function Expandable(props: Props) {
|
|||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{rect && rect.height > COLLAPSED_HEIGHT ? (
|
||||
{(rect && rect.height > COLLAPSED_HEIGHT) || beginCollapsed ? (
|
||||
<div ref={ref}>
|
||||
<div
|
||||
className={classnames({
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from 'react';
|
|||
import UriIndicator from 'component/uriIndicator';
|
||||
import { generateNotificationTitle } from './helpers/title';
|
||||
import { generateNotificationText } from './helpers/text';
|
||||
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
|
||||
const CommentCreate = lazyImport(() => import('component/commentCreate' /* webpackChunkName: "comments" */));
|
||||
const CommentReactions = lazyImport(() => import('component/commentReactions' /* webpackChunkName: "comments" */));
|
||||
|
@ -93,7 +94,7 @@ export default function Notification(props: Props) {
|
|||
let notificationLink = formatLbryUrlForWeb(notificationTarget);
|
||||
let urlParams = new URLSearchParams();
|
||||
if (isCommentNotification && notification_parameters.dynamic.hash) {
|
||||
urlParams.append('lc', notification_parameters.dynamic.hash);
|
||||
urlParams.append(LINKED_COMMENT_QUERY_PARAM, notification_parameters.dynamic.hash);
|
||||
}
|
||||
|
||||
let channelName;
|
||||
|
|
|
@ -266,9 +266,11 @@ function PublishForm(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
if (!modal) {
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setPreviewing(false);
|
||||
}, 250);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [modal]);
|
||||
|
||||
|
|
|
@ -58,7 +58,9 @@ function UserChannelFollowIntro(props: Props) {
|
|||
if (claimName) channelSubscribe(claimName, channelUri);
|
||||
});
|
||||
};
|
||||
setTimeout(delayedChannelSubscribe, 1000);
|
||||
const timer = setTimeout(delayedChannelSubscribe, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [prefsReady]);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const LINKED_COMMENT_QUERY_PARAM = 'lc';
|
||||
export const THREAD_COMMENT_QUERY_PARAM = 'tc';
|
||||
|
||||
export const SORT_COMMENTS_NEW = 'new';
|
||||
export const SORT_COMMENTS_BEST = 'best';
|
||||
|
|
|
@ -239,7 +239,7 @@ function AppWrapper() {
|
|||
if (readyToLaunch && persistDone) {
|
||||
app.store.dispatch(doDaemonReady());
|
||||
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (DEFAULT_LANGUAGE) {
|
||||
app.store.dispatch(doFetchLanguage(DEFAULT_LANGUAGE));
|
||||
}
|
||||
|
@ -251,6 +251,8 @@ function AppWrapper() {
|
|||
}, 25);
|
||||
|
||||
analytics.startupEvent(Date.now());
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [readyToLaunch, persistDone]);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||
import { makeSelectCollectionForId } from 'redux/selectors/collections';
|
||||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||
import { LINKED_COMMENT_QUERY_PARAM, THREAD_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { selectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent, selectClientSetting } from 'redux/selectors/settings';
|
||||
|
@ -33,7 +34,8 @@ const select = (state, props) => {
|
|||
|
||||
return {
|
||||
channelId: getChannelIdFromClaim(claim),
|
||||
linkedCommentId: urlParams.get('lc'),
|
||||
linkedCommentId: urlParams.get(LINKED_COMMENT_QUERY_PARAM),
|
||||
threadCommentId: urlParams.get(THREAD_COMMENT_QUERY_PARAM),
|
||||
costInfo: selectCostInfoForUri(state, uri),
|
||||
obscureNsfw: !selectShowMatureContent(state),
|
||||
isMature: selectClaimIsNsfwForUri(state, uri),
|
||||
|
|
|
@ -33,6 +33,7 @@ type Props = {
|
|||
obscureNsfw: boolean,
|
||||
isMature: boolean,
|
||||
linkedCommentId?: string,
|
||||
threadCommentId?: string,
|
||||
hasCollectionById?: boolean,
|
||||
collectionId: string,
|
||||
videoTheaterMode: boolean,
|
||||
|
@ -64,6 +65,7 @@ export default function FilePage(props: Props) {
|
|||
isMature,
|
||||
costInfo,
|
||||
linkedCommentId,
|
||||
threadCommentId,
|
||||
videoTheaterMode,
|
||||
|
||||
claimIsMine,
|
||||
|
@ -104,7 +106,7 @@ export default function FilePage(props: Props) {
|
|||
}, [audioVideoDuration, fileInfo, position]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (linkedCommentId && isMobile) {
|
||||
if ((linkedCommentId || threadCommentId) && isMobile) {
|
||||
doToggleAppDrawer();
|
||||
}
|
||||
// only on mount, otherwise clicking on a comments timestamp and linking it
|
||||
|
@ -215,7 +217,7 @@ export default function FilePage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const commentsListProps = { uri, linkedCommentId };
|
||||
const commentsListProps = { uri, linkedCommentId, threadCommentId };
|
||||
const emptyMsgProps = { padded: !isMobile };
|
||||
|
||||
return (
|
||||
|
@ -254,7 +256,7 @@ export default function FilePage(props: Props) {
|
|||
<DrawerExpandButton label={commentsListTitle} />
|
||||
</>
|
||||
) : (
|
||||
<CommentsList {...commentsListProps} />
|
||||
<CommentsList {...commentsListProps} notInDrawer />
|
||||
)}
|
||||
</React.Suspense>
|
||||
</section>
|
||||
|
@ -269,7 +271,7 @@ export default function FilePage(props: Props) {
|
|||
: !contentCommentsDisabled && (
|
||||
<div className="file-page__post-comments">
|
||||
<React.Suspense fallback={null}>
|
||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />
|
||||
<CommentsList {...commentsListProps} commentsAreExpanded notInDrawer />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import { DOMAIN, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import { LINKED_COMMENT_QUERY_PARAM, THREAD_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
import React, { useEffect } from 'react';
|
||||
import { lazyImport } from 'util/lazyImport';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
@ -68,7 +69,8 @@ export default function ShowPage(props: Props) {
|
|||
|
||||
const { search, pathname, hash } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const linkedCommentId = urlParams.get('lc');
|
||||
const linkedCommentId = urlParams.get(LINKED_COMMENT_QUERY_PARAM);
|
||||
const threadCommentId = urlParams.get(THREAD_COMMENT_QUERY_PARAM);
|
||||
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const canonicalUrl = claim && claim.canonical_url;
|
||||
|
@ -134,6 +136,7 @@ export default function ShowPage(props: Props) {
|
|||
isMine === undefined && isAuthenticated ? { include_is_my_output: true, include_purchase_receipt: true } : {}
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
doResolveUri,
|
||||
isResolvingUri,
|
||||
|
@ -250,7 +253,12 @@ export default function ShowPage(props: Props) {
|
|||
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<FilePage uri={uri} collectionId={collectionId} linkedCommentId={linkedCommentId} />
|
||||
<FilePage
|
||||
uri={uri}
|
||||
collectionId={collectionId}
|
||||
linkedCommentId={linkedCommentId}
|
||||
threadCommentId={threadCommentId}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const defaultState: CommentsState = {
|
|||
// Remove commentsByUri
|
||||
// It is not needed and doesn't provide anything but confusion
|
||||
commentsByUri: {}, // URI -> claimId
|
||||
linkedCommentAncestors: {},
|
||||
fetchedCommentAncestors: {},
|
||||
superChatsByUri: {},
|
||||
pinnedCommentsById: {},
|
||||
isLoading: false,
|
||||
|
@ -386,7 +386,7 @@ export default handleActions(
|
|||
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||
const linkedCommentAncestors = Object.assign({}, state.linkedCommentAncestors);
|
||||
const fetchedCommentAncestors = Object.assign({}, state.fetchedCommentAncestors);
|
||||
|
||||
const updateStore = (comment, commentById, byId, repliesByParentId, topLevelCommentsById) => {
|
||||
commentById[comment.comment_id] = comment;
|
||||
|
@ -409,7 +409,7 @@ export default handleActions(
|
|||
if (ancestors) {
|
||||
ancestors.forEach((ancestor) => {
|
||||
updateStore(ancestor, commentById, byId, repliesByParentId, topLevelCommentsById);
|
||||
immPushToArrayInObject(linkedCommentAncestors, comment.comment_id, ancestor.comment_id);
|
||||
immPushToArrayInObject(fetchedCommentAncestors, comment.comment_id, ancestor.comment_id);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -423,7 +423,7 @@ export default handleActions(
|
|||
repliesByParentId,
|
||||
byId,
|
||||
commentById,
|
||||
linkedCommentAncestors,
|
||||
fetchedCommentAncestors,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -162,8 +162,11 @@ export const selectTopLevelCommentsByClaimId = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const makeSelectCommentForCommentId = (commentId: string) =>
|
||||
createSelector(selectCommentsById, (comments) => comments[commentId]);
|
||||
export const selectCommentForCommentId = createSelector(
|
||||
(state, commentId) => commentId,
|
||||
selectCommentsById,
|
||||
(commentId, comments) => comments[commentId]
|
||||
);
|
||||
|
||||
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||
const byParentId = state.repliesByParentId || {};
|
||||
|
@ -182,7 +185,13 @@ export const selectRepliesByParentId = createSelector(selectState, selectComment
|
|||
return comments;
|
||||
});
|
||||
|
||||
export const selectLinkedCommentAncestors = (state: State) => selectState(state).linkedCommentAncestors;
|
||||
export const selectFetchedCommentAncestors = (state: State) => selectState(state).fetchedCommentAncestors;
|
||||
|
||||
export const selectCommentAncestorsForId = createSelector(
|
||||
(state, commentId) => commentId,
|
||||
selectFetchedCommentAncestors,
|
||||
(commentId, fetchedAncestors) => fetchedAncestors && fetchedAncestors[commentId]
|
||||
);
|
||||
|
||||
export const selectCommentIdsForUri = (state: State, uri: string) => {
|
||||
const claimId = selectClaimIdForUri(state, uri);
|
||||
|
|
|
@ -19,23 +19,6 @@
|
|||
.card__main-actions {
|
||||
margin-top: 0px !important;
|
||||
|
||||
.comment__sort {
|
||||
margin: 0px !important;
|
||||
display: inline;
|
||||
|
||||
button {
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .button--alt {
|
||||
padding: var(--spacing-xxs) var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__sort + button {
|
||||
margin: 0px var(--spacing-xxs);
|
||||
}
|
||||
|
@ -44,6 +27,23 @@
|
|||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__sort {
|
||||
margin: 0px !important;
|
||||
display: inline;
|
||||
|
||||
+ .button--alt {
|
||||
padding: var(--spacing-xxs) var(--spacing-s);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,17 +279,20 @@
|
|||
}
|
||||
|
||||
.card--enable-overflow {
|
||||
.card__header--between {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.card__header--between {
|
||||
margin-bottom: 0;
|
||||
|
||||
.card__title-section {
|
||||
width: 60%;
|
||||
margin: var(--spacing-s) 0px;
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card__title-actions-container {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.card__title-actions-container {
|
||||
flex-grow: 1;
|
||||
min-height: 47px;
|
||||
|
||||
|
|
|
@ -34,10 +34,9 @@ $thumbnailWidthSmall: 2rem;
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.comments--replies {
|
||||
list-style-type: none;
|
||||
margin-left: var(--spacing-s);
|
||||
flex: 1;
|
||||
.comment__list .card__first-pane > .button {
|
||||
margin-left: 3px;
|
||||
height: calc(var(--height-button) - 3px);
|
||||
}
|
||||
|
||||
.comment__sort {
|
||||
|
@ -56,7 +55,7 @@ $thumbnailWidthSmall: 2rem;
|
|||
.comment {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
font-size: var(--font-small);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
@ -88,13 +87,17 @@ $thumbnailWidthSmall: 2rem;
|
|||
}
|
||||
|
||||
.comment__thumbnail-wrapper {
|
||||
flex: 0;
|
||||
// margin-top: var(--spacing-xxs);
|
||||
display: grid;
|
||||
justify-items: stretch;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
.ff-canvas {
|
||||
|
@ -113,19 +116,49 @@ $thumbnailWidthSmall: 2rem;
|
|||
}
|
||||
|
||||
.comment__replies {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-m);
|
||||
margin-left: #{$thumbnailWidthSmall};
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
|
||||
}
|
||||
list-style-type: none;
|
||||
margin-left: var(--spacing-s);
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__replies-loading {
|
||||
color: var(--color-link);
|
||||
align-items: center;
|
||||
margin-left: var(--spacing-xs);
|
||||
|
||||
.spinner {
|
||||
margin: 0px;
|
||||
|
||||
.rect {
|
||||
background-color: var(--color-link) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__replies-loading--more {
|
||||
align-items: flex-start;
|
||||
transform: translate(var(--spacing-xs));
|
||||
|
||||
.spinner {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__thread-links {
|
||||
font-size: var(--font-xsmall);
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
.button {
|
||||
padding: var(--spacing-xxs) 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment--reply {
|
||||
margin: 0;
|
||||
|
||||
|
@ -151,18 +184,19 @@ $thumbnailWidthSmall: 2rem;
|
|||
.comment__threadline {
|
||||
@extend .button--alt;
|
||||
height: auto;
|
||||
align-self: stretch;
|
||||
padding: 1px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-comment-threadline);
|
||||
border-left: 1px solid var(--color-comment-threadline);
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px;
|
||||
left: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-comment-threadline-hover);
|
||||
border-color: var(--color-comment-threadline-hover);
|
||||
}
|
||||
border-left: 4px solid var(--color-comment-threadline);
|
||||
background-color: transparent !important;
|
||||
padding-right: calc(var(--spacing-m) - 3px);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: 2px;
|
||||
@media (max-width: $breakpoint-small) {
|
||||
left: calc(50% - 3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -238,18 +238,16 @@ body {
|
|||
.card {
|
||||
padding: 0;
|
||||
.card__first-pane {
|
||||
.card__main-actions {
|
||||
.button--alt:last-of-type {
|
||||
top: -1px;
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
.comment__sort {
|
||||
.button--alt:last-of-type {
|
||||
top: unset;
|
||||
float: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
.button--alt[aria-label='Refresh'] {
|
||||
top: -1px;
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
.comment__sort {
|
||||
.button--alt[aria-label='Refresh'] {
|
||||
top: unset;
|
||||
float: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -270,18 +270,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card__main-actions {
|
||||
.button--alt:last-of-type {
|
||||
top: -1px;
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
.comment__sort {
|
||||
.button--alt:last-of-type {
|
||||
top: unset;
|
||||
float: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
.button--alt[aria-label='Refresh'] {
|
||||
top: -1px;
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
.comment__sort {
|
||||
.button--alt[aria-label='Refresh'] {
|
||||
top: unset;
|
||||
float: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue