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