comments v69

This commit is contained in:
Sean Yesmunt 2020-10-07 15:14:52 -04:00
parent 3e70d5a398
commit c43eff8587
10 changed files with 70 additions and 82 deletions

View file

@ -1308,8 +1308,7 @@
"Downvote": "Downvote", "Downvote": "Downvote",
"Best": "Best", "Best": "Best",
"Controversial": "Controversial", "Controversial": "Controversial",
"Hide %number% Replies": "Hide %number% Replies", "Show Replies": "Show Replies",
"Show %number% Replies": "Show %number% Replies",
"Unable to create comment, please try again later.": "Unable to create comment, please try again later.", "Unable to create comment, please try again later.": "Unable to create comment, please try again later.",
"Your channel is still being setup, try again in a few moments.": "Your channel is still being setup, try again in a few moments.", "Your channel is still being setup, try again in a few moments.": "Your channel is still being setup, try again in a few moments.",
"Unable to delete this comment, please try again later.": "Unable to delete this comment, please try again later.", "Unable to delete this comment, please try again later.": "Unable to delete this comment, please try again later.",

View file

@ -18,13 +18,13 @@ import usePersistedState from 'effects/use-persisted-state';
import CommentReactions from 'component/commentReactions'; import CommentReactions from 'component/commentReactions';
import CommentsReplies from 'component/commentsReplies'; import CommentsReplies from 'component/commentsReplies';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import CommentCreate from 'component/commentCreate';
type Props = { type Props = {
uri: string, uri: string,
author: ?string, // LBRY Channel Name, e.g. @channel author: ?string, // LBRY Channel Name, e.g. @channel
authorUri: string, // full LBRY Channel URI: lbry://@channel#123... authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
commentId: string, // sha256 digest identifying the comment commentId: string, // sha256 digest identifying the comment
topLevelId: string, // sha256 digest identifying the parent of the comment
message: string, // comment body message: string, // comment body
timePosted: number, // Comment timestamp timePosted: number, // Comment timestamp
channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail
@ -41,10 +41,8 @@ type Props = {
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
commentingEnabled: boolean, commentingEnabled: boolean,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
hideReplyButton?: boolean,
isTopLevel?: boolean, isTopLevel?: boolean,
topLevelIsReplying: boolean, threadDepth: number,
setTopLevelIsReplying: boolean => void,
}; };
const LENGTH_TO_COLLAPSE = 300; const LENGTH_TO_COLLAPSE = 300;
@ -71,11 +69,8 @@ function Comment(props: Props) {
commentingEnabled, commentingEnabled,
myChannels, myChannels,
doToast, doToast,
hideReplyButton,
isTopLevel, isTopLevel,
topLevelIsReplying, threadDepth,
setTopLevelIsReplying,
topLevelId,
} = props; } = props;
const { const {
push, push,
@ -134,17 +129,14 @@ function Comment(props: Props) {
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`); push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) }); doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) });
} else { } else {
if (setTopLevelIsReplying) { setReplying(!isReplying);
setTopLevelIsReplying(!topLevelIsReplying);
} else {
setReplying(!isReplying);
}
} }
} }
return ( return (
<li <li
className={classnames('comment', { className={classnames('comment', {
'comment--top-level': isTopLevel,
'comment--reply': !isTopLevel, 'comment--reply': !isTopLevel,
'comment--highlighted': linkedComment && linkedComment.comment_id === commentId, 'comment--highlighted': linkedComment && linkedComment.comment_id === commentId,
})} })}
@ -242,7 +234,7 @@ function Comment(props: Props) {
</div> </div>
<div className="comment__actions"> <div className="comment__actions">
{!hideReplyButton && ( {threadDepth !== 0 && (
<Button <Button
requiresAuth={IS_WEB} requiresAuth={IS_WEB}
label={commentingEnabled ? __('Reply') : __('Log in to reply')} label={commentingEnabled ? __('Reply') : __('Log in to reply')}
@ -253,21 +245,23 @@ function Comment(props: Props) {
)} )}
{ENABLE_COMMENT_REACTIONS && <CommentReactions commentId={commentId} />} {ENABLE_COMMENT_REACTIONS && <CommentReactions commentId={commentId} />}
</div> </div>
{isReplying && (
<CommentCreate
isReply
uri={uri}
parentId={commentId}
onDoneReplying={() => setReplying(false)}
onCancelReplying={() => setReplying(false)}
/>
)}
</> </>
)} )}
</div> </div>
</div> </div>
</div> </div>
{isTopLevel && ( <CommentsReplies threadDepth={threadDepth - 1} uri={uri} parentId={commentId} linkedComment={linkedComment} />
<CommentsReplies
uri={uri}
topLevelId={topLevelId}
linkedComment={linkedComment}
topLevelIsReplying={isReplying}
setTopLevelIsReplying={setReplying}
/>
)}
</li> </li>
); );
} }

View file

@ -1,15 +1,17 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux';
import { selectIsPostingComment } from 'redux/selectors/comments';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { doCommentCreate } from 'redux/actions/comments'; import { doCommentCreate } from 'redux/actions/comments';
import { CommentCreate } from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { CommentCreate } from './view';
const select = (state, props) => ({ const select = (state, props) => ({
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
isFetchingChannels: selectFetchingMyChannels(state), isFetchingChannels: selectFetchingMyChannels(state),
isPostingComment: selectIsPostingComment(state),
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({

View file

@ -17,11 +17,13 @@ type Props = {
claim: StreamClaim, claim: StreamClaim,
createComment: (string, string, string, ?string) => Promise<any>, createComment: (string, string, string, ?string) => Promise<any>,
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
topLevelId?: string,
onDoneReplying?: () => void, onDoneReplying?: () => void,
onCancelReplying?: () => void, onCancelReplying?: () => void,
isNested: boolean, isNested: boolean,
isFetchingChannels: boolean, isFetchingChannels: boolean,
parentId: string,
isReply: boolean,
isPostingComment: boolean,
}; };
export function CommentCreate(props: Props) { export function CommentCreate(props: Props) {
@ -29,23 +31,23 @@ export function CommentCreate(props: Props) {
createComment, createComment,
claim, claim,
channels, channels,
topLevelId,
onDoneReplying, onDoneReplying,
onCancelReplying, onCancelReplying,
isNested, isNested,
isFetchingChannels, isFetchingChannels,
isReply,
parentId,
isPostingComment,
} = props; } = props;
const buttonref: ElementRef<any> = React.useRef(); const buttonref: ElementRef<any> = React.useRef();
const { push } = useHistory(); const { push } = useHistory();
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
const isReply = !!topLevelId;
const [commentValue, setCommentValue] = React.useState(''); const [commentValue, setCommentValue] = React.useState('');
const [channel, setChannel] = usePersistedState('comment-channel', ''); const [channel, setChannel] = usePersistedState('comment-channel', '');
const [charCount, setCharCount] = useState(commentValue.length); const [charCount, setCharCount] = useState(commentValue.length);
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
const hasChannels = channels && channels.length; const hasChannels = channels && channels.length;
const disabled = channel === CHANNEL_NEW || !commentValue.length; const disabled = isPostingComment || channel === CHANNEL_NEW || !commentValue.length;
const topChannel = const topChannel =
channels && channels &&
channels.reduce((top, channel) => { channels.reduce((top, channel) => {
@ -90,9 +92,10 @@ export function CommentCreate(props: Props) {
function handleSubmit() { function handleSubmit() {
if (channel !== CHANNEL_NEW && commentValue.length) { if (channel !== CHANNEL_NEW && commentValue.length) {
createComment(commentValue, claimId, channel, topLevelId).then(res => { createComment(commentValue, claimId, channel, parentId).then(res => {
if (res && res.signature) { if (res && res.signature) {
setCommentValue(''); setCommentValue('');
if (onDoneReplying) { if (onDoneReplying) {
onDoneReplying(); onDoneReplying();
} }
@ -160,7 +163,15 @@ export function CommentCreate(props: Props) {
button="primary" button="primary"
disabled={disabled} disabled={disabled}
type="submit" type="submit"
label={isReply ? __('Reply') : __('Post')} label={
isReply
? isPostingComment
? __('Replying...')
: __('Reply')
: isPostingComment
? __('Posting...')
: __('Post')
}
requiresAuth={IS_WEB} requiresAuth={IS_WEB}
/> />
{isReply && ( {isReply && (

View file

@ -193,7 +193,9 @@ function CommentList(props: Props) {
actions={ actions={
<> <>
<CommentCreate uri={uri} /> <CommentCreate uri={uri} />
{!isFetchingComments && hasNoComments && <div className="main--empty">{__('Be the first to comment!')}</div>} {!isFetchingComments && hasNoComments && <div className="main--empty">{__('Be the first to comment!')}</div>}
<ul className="comments" ref={commentRef}> <ul className="comments" ref={commentRef}>
{!isFetchingComments && {!isFetchingComments &&
comments && comments &&
@ -202,13 +204,13 @@ function CommentList(props: Props) {
return ( return (
<CommentView <CommentView
isTopLevel isTopLevel
threadDepth={3}
key={comment.comment_id} key={comment.comment_id}
uri={uri} uri={uri}
authorUri={comment.channel_url} authorUri={comment.channel_url}
author={comment.channel_name} author={comment.channel_name}
claimId={comment.claim_id} claimId={comment.claim_id}
commentId={comment.comment_id} commentId={comment.comment_id}
topLevelId={comment.comment_id}
message={comment.comment} message={comment.comment}
timePosted={comment.timestamp * 1000} timePosted={comment.timestamp * 1000}
claimIsMine={claimIsMine} claimIsMine={claimIsMine}
@ -218,6 +220,7 @@ function CommentList(props: Props) {
); );
})} })}
</ul> </ul>
{(isFetchingComments || moreBelow) && ( {(isFetchingComments || moreBelow) && (
<div className="main--empty" ref={spinnerRef}> <div className="main--empty" ref={spinnerRef}>
<Spinner type="small" /> <Spinner type="small" />

View file

@ -5,7 +5,7 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
import CommentsReplies from './view'; import CommentsReplies from './view';
const select = (state, props) => ({ const select = (state, props) => ({
comments: makeSelectRepliesForParentId(props.topLevelId)(state), comments: makeSelectRepliesForParentId(props.parentId)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
myChannels: selectMyChannelClaims(state), myChannels: selectMyChannelClaims(state),

View file

@ -3,7 +3,6 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import Comment from 'component/comment'; import Comment from 'component/comment';
import Button from 'component/button'; import Button from 'component/button';
import CommentCreate from 'component/commentCreate';
type Props = { type Props = {
comments: Array<any>, comments: Array<any>,
@ -11,25 +10,13 @@ type Props = {
claimIsMine: boolean, claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
linkedComment?: Comment, linkedComment?: Comment,
topLevelId: string,
commentingEnabled: boolean, commentingEnabled: boolean,
topLevelIsReplying: boolean, threadDepth: number,
setTopLevelIsReplying: boolean => void,
}; };
function CommentsReplies(props: Props) { function CommentsReplies(props: Props) {
const { const { uri, comments, claimIsMine, myChannels, linkedComment, commentingEnabled, threadDepth } = props;
uri, const [isExpanded, setExpanded] = React.useState(true);
comments,
claimIsMine,
myChannels,
linkedComment,
topLevelId,
commentingEnabled,
topLevelIsReplying,
setTopLevelIsReplying,
} = props;
const [isExpanded, setExpanded] = React.useState(false);
const [start, setStart] = React.useState(0); const [start, setStart] = React.useState(0);
const [end, setEnd] = React.useState(9); const [end, setEnd] = React.useState(9);
const sortedComments = comments ? [...comments].reverse() : []; const sortedComments = comments ? [...comments].reverse() : [];
@ -63,7 +50,6 @@ function CommentsReplies(props: Props) {
setStart(numberOfComments || 0); setStart(numberOfComments || 0);
} }
setEnd(numberOfComments + 1); setEnd(numberOfComments + 1);
setTopLevelIsReplying(false);
} }
React.useEffect(() => { React.useEffect(() => {
@ -84,17 +70,13 @@ function CommentsReplies(props: Props) {
const displayedComments = sortedComments.slice(start, end); const displayedComments = sortedComments.slice(start, end);
return ( return (
(Boolean(numberOfComments) || topLevelIsReplying) && ( Boolean(numberOfComments) && (
<div className="comment__replies-container"> <div className="comment__replies-container">
{Boolean(numberOfComments) && ( {Boolean(numberOfComments) && !isExpanded && (
<div className="comment__actions--nested"> <div className="comment__actions--nested">
<Button <Button
className="comment__action" className="comment__action"
label={ label={__('Show Replies')}
isExpanded
? __('Hide %number% Replies', { number: numberOfComments })
: __('Show %number% Replies', { number: numberOfComments })
}
onClick={() => setExpanded(!isExpanded)} onClick={() => setExpanded(!isExpanded)}
icon={isExpanded ? ICONS.UP : ICONS.DOWN} icon={isExpanded ? ICONS.UP : ICONS.DOWN}
/> />
@ -109,6 +91,7 @@ function CommentsReplies(props: Props) {
{displayedComments.map((comment, index) => { {displayedComments.map((comment, index) => {
return ( return (
<Comment <Comment
threadDepth={threadDepth}
uri={uri} uri={uri}
authorUri={comment.channel_url} authorUri={comment.channel_url}
author={comment.channel_name} author={comment.channel_name}
@ -121,9 +104,7 @@ function CommentsReplies(props: Props) {
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)} commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
linkedComment={linkedComment} linkedComment={linkedComment}
commentingEnabled={commentingEnabled} commentingEnabled={commentingEnabled}
hideReplyButton={index !== displayedComments.length - 1} handleCommentDone={handleCommentDone}
topLevelIsReplying={topLevelIsReplying}
setTopLevelIsReplying={setTopLevelIsReplying}
/> />
); );
})} })}
@ -136,17 +117,6 @@ function CommentsReplies(props: Props) {
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" /> <Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
</div> </div>
)} )}
{topLevelIsReplying && (
<CommentCreate
isNested={isExpanded}
key={topLevelId}
uri={uri}
topLevelId={topLevelId}
onDoneReplying={() => handleCommentDone()}
onCancelReplying={() => setTopLevelIsReplying(false)}
/>
)}
</div> </div>
) )
); );

View file

@ -1,5 +1,5 @@
$thumbnailWidth: 2rem; $thumbnailWidth: 1.5rem;
$thumbnailWidthSmall: 1.5rem; $thumbnailWidthSmall: 0rem;
.comments { .comments {
list-style-type: none; list-style-type: none;
@ -9,7 +9,7 @@ $thumbnailWidthSmall: 1.5rem;
.comments--replies { .comments--replies {
list-style-type: none; list-style-type: none;
margin-left: var(--spacing-m); margin-left: var(--spacing-s);
flex: 1; flex: 1;
} }
@ -29,8 +29,7 @@ $thumbnailWidthSmall: 1.5rem;
} }
.comment__create--reply { .comment__create--reply {
margin-top: var(--spacing-l); margin-top: var(--spacing-m);
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
position: relative; position: relative;
} }
@ -81,11 +80,17 @@ $thumbnailWidthSmall: 1.5rem;
} }
} }
.comment--top-level {
&:not(:first-child) {
margin-top: var(--spacing-l);
}
}
.comment__threadline { .comment__threadline {
@extend .button--alt; @extend .button--alt;
height: auto; height: auto;
align-self: stretch; align-self: stretch;
padding: 2px; padding: 1px;
border-radius: 3px; border-radius: 3px;
background-color: var(--color-comment-threadline); background-color: var(--color-comment-threadline);
@ -94,6 +99,10 @@ $thumbnailWidthSmall: 1.5rem;
background-color: var(--color-comment-threadline-hover); background-color: var(--color-comment-threadline-hover);
border-color: var(--color-comment-threadline-hover); border-color: var(--color-comment-threadline-hover);
} }
@media (min-width: $breakpoint-small) {
padding: 2px;
}
} }
.comment-new__label-wrapper { .comment-new__label-wrapper {
@ -134,7 +143,6 @@ $thumbnailWidthSmall: 1.5rem;
.comment__meta { .comment__meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
text-overflow: ellipsis;
} }
.comment__meta-information { .comment__meta-information {

View file

@ -57,6 +57,8 @@
--color-purchased-text: var(--color-gray-5); --color-purchased-text: var(--color-gray-5);
--color-comment-highlighted: #484734; --color-comment-highlighted: #484734;
--color-thumbnail-background: var(--color-gray-5); --color-thumbnail-background: var(--color-gray-5);
--color-comment-threadline: #434b54;
--color-comment-threadline-hover: var(--color-gray-4);
// Text // Text
--color-text: #d8d8d8; --color-text: #d8d8d8;

View file

@ -26,9 +26,8 @@
--color-purchased-alt: #ffebc2; --color-purchased-alt: #ffebc2;
--color-purchased-text: var(--color-gray-5); --color-purchased-text: var(--color-gray-5);
--color-comment-highlighted: #fff2d9; --color-comment-highlighted: #fff2d9;
--color-comment-threadline: var(--color-gray-2); --color-comment-threadline: var(--color-gray-1);
--color-comment-threadline-hover: var(--color-gray-4); --color-comment-threadline-hover: var(--color-gray-4);
--color-comment-threadline-border: var(--color-gray-2);
--color-thumbnail-background: var(--color-gray-1); --color-thumbnail-background: var(--color-gray-1);
// Icons // Icons