add placeholder upvote/downvote buttons on comments
This commit is contained in:
parent
d493a5f9ea
commit
b0d19455c1
14 changed files with 340 additions and 227 deletions
|
@ -5,13 +5,15 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectIsUriResolving,
|
||||
selectMyChannelClaims,
|
||||
} from 'lbry-redux';
|
||||
import { doCommentAbandon, doCommentUpdate } from 'redux/actions/comments';
|
||||
import { doToggleBlockChannel } from 'redux/actions/blocked';
|
||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
||||
import Comment from './view';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectIsFetchingComments } from 'redux/selectors/comments';
|
||||
import Comment from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
|
||||
|
@ -21,6 +23,7 @@ const select = (state, props) => ({
|
|||
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -28,6 +31,7 @@ const perform = dispatch => ({
|
|||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
||||
deleteComment: commentId => dispatch(doCommentAbandon(commentId)),
|
||||
blockChannel: channelUri => dispatch(doToggleBlockChannel(channelUri)),
|
||||
doToast: options => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Comment);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
import { SITE_NAME, SIMPLE_SITE } from 'config';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { isEmpty } from 'util/object';
|
||||
import DateTime from 'component/dateTime';
|
||||
import Button from 'component/button';
|
||||
// import Expandable from 'component/expandable';
|
||||
import Expandable from 'component/expandable';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
|
@ -14,13 +15,16 @@ import Icon from 'component/common/icon';
|
|||
import { FormField, Form } from 'component/common/form';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
import CommentsReplies from 'component/commentsReplies';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
author: ?string, // LBRY Channel Name, e.g. @channel
|
||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
||||
commentId: string, // sha256 digest identifying the comment
|
||||
parentId: string, // sha256 digest identifying the parent of the comment
|
||||
topLevelId: string, // sha256 digest identifying the parent of the comment
|
||||
message: string, // comment body
|
||||
timePosted: number, // Comment timestamp
|
||||
channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail
|
||||
|
@ -34,6 +38,13 @@ type Props = {
|
|||
deleteComment: string => void,
|
||||
blockChannel: string => void,
|
||||
linkedComment?: any,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
commentingEnabled: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
hideReplyButton?: boolean,
|
||||
isTopLevel?: boolean,
|
||||
topLevelIsReplying: boolean,
|
||||
setTopLevelIsReplying: boolean => void,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -53,20 +64,31 @@ function Comment(props: Props) {
|
|||
channelIsBlocked,
|
||||
commentIsMine,
|
||||
commentId,
|
||||
parentId,
|
||||
updateComment,
|
||||
deleteComment,
|
||||
blockChannel,
|
||||
linkedComment,
|
||||
commentingEnabled,
|
||||
myChannels,
|
||||
doToast,
|
||||
hideReplyButton,
|
||||
isTopLevel,
|
||||
topLevelIsReplying,
|
||||
setTopLevelIsReplying,
|
||||
topLevelId,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const [editedMessage, setCommentValue] = useState(message);
|
||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
||||
|
||||
// used for controlling the visibility of the menu icon
|
||||
const [mouseIsHovering, setMouseHover] = useState(false);
|
||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const hasChannels = myChannels && myChannels.length > 0;
|
||||
|
||||
// to debounce subsequent requests
|
||||
const shouldFetch =
|
||||
|
@ -107,106 +129,149 @@ function Comment(props: Props) {
|
|||
setEditing(false);
|
||||
}
|
||||
|
||||
function handleCommentReply() {
|
||||
if (!hasChannels) {
|
||||
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
|
||||
doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) });
|
||||
} else {
|
||||
if (setTopLevelIsReplying) {
|
||||
setTopLevelIsReplying(!topLevelIsReplying);
|
||||
} else {
|
||||
setReplying(!isReplying);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames('comment', {
|
||||
comment__reply: parentId !== null,
|
||||
comment__highlighted: linkedComment && linkedComment.comment_id === commentId,
|
||||
'comment--reply': !isTopLevel,
|
||||
'comment--highlighted': linkedComment && linkedComment.comment_id === commentId,
|
||||
})}
|
||||
id={commentId}
|
||||
onMouseOver={() => setMouseHover(true)}
|
||||
onMouseOut={() => setMouseHover(false)}
|
||||
>
|
||||
<div className="comment__author-thumbnail">
|
||||
{authorUri ? <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small /> : <ChannelThumbnail small />}
|
||||
</div>
|
||||
|
||||
<div className="comment__body_container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{!author ? (
|
||||
<span className="comment__author">{__('Anonymous')}</span>
|
||||
) : (
|
||||
<Button
|
||||
className="button--uri-indicator truncated-text comment__author"
|
||||
navigate={authorUri}
|
||||
label={author}
|
||||
/>
|
||||
)}
|
||||
{/* // link here */}
|
||||
<Button
|
||||
navigate={`${uri}?lc=${commentId}`}
|
||||
label={
|
||||
<time className="comment__time" dateTime={timePosted}>
|
||||
{DateTime.getTimeAgoStr(timePosted)}
|
||||
</time>
|
||||
}
|
||||
className="button--uri-indicator"
|
||||
/>
|
||||
</div>
|
||||
<div className="comment__menu">
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
size={18}
|
||||
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
|
||||
icon={ICONS.MORE_VERTICAL}
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list--comments">
|
||||
{commentIsMine ? (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => setEditing(true)}>
|
||||
{__('Edit')}
|
||||
</MenuItem>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => deleteComment(commentId)}>
|
||||
{__('Delete')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem className="comment__menu-option" onSelect={() => blockChannel(authorUri)}>
|
||||
{__('Block Channel')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
value={editedMessage}
|
||||
charCount={charCount}
|
||||
onChange={handleEditMessageChanged}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={__('Done')}
|
||||
requiresAuth={IS_WEB}
|
||||
disabled={message === editedMessage}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setEditing(false)} />
|
||||
</div>
|
||||
</Form>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<div className="comment__message">
|
||||
{/* <Expandable> */}
|
||||
<MarkdownPreview content={message} />
|
||||
{/* </Expandable> */}
|
||||
</div>
|
||||
<div className="comment__content">
|
||||
<div className="comment__author-thumbnail">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small />
|
||||
) : (
|
||||
<div className="comment__message">
|
||||
<MarkdownPreview content={message} />
|
||||
</div>
|
||||
<ChannelThumbnail small />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="comment__body_container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{!author ? (
|
||||
<span className="comment__author">{__('Anonymous')}</span>
|
||||
) : (
|
||||
<Button
|
||||
className="button--uri-indicator truncated-text comment__author"
|
||||
navigate={authorUri}
|
||||
label={author}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="button--uri-indicator"
|
||||
navigate={`${uri}?lc=${commentId}`}
|
||||
label={
|
||||
<time className="comment__time" dateTime={timePosted}>
|
||||
{DateTime.getTimeAgoStr(timePosted)}
|
||||
</time>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="comment__menu">
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
size={18}
|
||||
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
|
||||
icon={ICONS.MORE_VERTICAL}
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list--comments">
|
||||
{commentIsMine ? (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => setEditing(true)}>
|
||||
{__('Edit')}
|
||||
</MenuItem>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => deleteComment(commentId)}>
|
||||
{__('Delete')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem className="comment__menu-option" onSelect={() => blockChannel(authorUri)}>
|
||||
{__('Block Channel')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
value={editedMessage}
|
||||
charCount={charCount}
|
||||
onChange={handleEditMessageChanged}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={__('Done')}
|
||||
requiresAuth={IS_WEB}
|
||||
disabled={message === editedMessage}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setEditing(false)} />
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<>
|
||||
<div className="comment__message">
|
||||
{editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview content={message} />
|
||||
</Expandable>
|
||||
) : (
|
||||
<MarkdownPreview content={message} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="comment__actions">
|
||||
<CommentReactions />
|
||||
{!hideReplyButton && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTopLevel && (
|
||||
<CommentsReplies
|
||||
uri={uri}
|
||||
topLevelId={topLevelId}
|
||||
linkedComment={linkedComment}
|
||||
topLevelIsReplying={isReplying}
|
||||
setTopLevelIsReplying={setReplying}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,17 +18,17 @@ type Props = {
|
|||
openModal: (id: string, { onCommentAcknowledge: () => void }) => void,
|
||||
createComment: (string, string, string, ?string) => void,
|
||||
channels: ?Array<ChannelClaim>,
|
||||
parentId?: string,
|
||||
topLevelId?: string,
|
||||
onDoneReplying?: () => void,
|
||||
onCancelReplying?: () => void,
|
||||
isNested: boolean,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
const { createComment, claim, openModal, channels, parentId, onDoneReplying, onCancelReplying, isNested } = props;
|
||||
const { createComment, claim, openModal, channels, topLevelId, onDoneReplying, onCancelReplying, isNested } = props;
|
||||
const { push } = useHistory();
|
||||
const { claim_id: claimId } = claim;
|
||||
const isReply = !!parentId;
|
||||
const isReply = !!topLevelId;
|
||||
const [commentValue, setCommentValue] = React.useState('');
|
||||
const [commentAck, setCommentAck] = usePersistedState('comment-acknowledge', false);
|
||||
const [channel, setChannel] = usePersistedState('comment-channel', '');
|
||||
|
@ -74,9 +74,11 @@ export function CommentCreate(props: Props) {
|
|||
|
||||
function handleSubmit() {
|
||||
if (channel !== CHANNEL_NEW && commentValue.length) {
|
||||
createComment(commentValue, claimId, channel, parentId);
|
||||
createComment(commentValue, claimId, channel, topLevelId);
|
||||
}
|
||||
|
||||
setCommentValue('');
|
||||
|
||||
if (onDoneReplying) {
|
||||
onDoneReplying();
|
||||
}
|
||||
|
|
8
ui/component/commentReactions/index.js
Normal file
8
ui/component/commentReactions/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Comment from './view';
|
||||
|
||||
const select = (state, props) => ({});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(Comment);
|
33
ui/component/commentReactions/view.jsx
Normal file
33
ui/component/commentReactions/view.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
myReaction: ?string,
|
||||
};
|
||||
|
||||
export default function CommentReactions(props: Props) {
|
||||
const { myReaction } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title={__('Upvote')}
|
||||
icon={ICONS.UPVOTE}
|
||||
className={classnames('comment__action', {
|
||||
'comment__action--active': myReaction === REACTION_TYPES.LIKE,
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
title={__('Downvote')}
|
||||
icon={ICONS.DOWNVOTE}
|
||||
className={classnames('comment__action', {
|
||||
'comment__action--active': myReaction === REACTION_TYPES.DISLIKE,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,6 @@ import Spinner from 'component/spinner';
|
|||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import CommentsReplies from 'component/commentsReplies';
|
||||
|
||||
type Props = {
|
||||
comments: Array<any>,
|
||||
|
@ -77,7 +76,6 @@ function CommentList(props: Props) {
|
|||
}
|
||||
|
||||
const displayedComments = prepareComments(comments, linkedComment).slice(start, end);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
|
@ -102,22 +100,21 @@ function CommentList(props: Props) {
|
|||
displayedComments &&
|
||||
displayedComments.map(comment => {
|
||||
return (
|
||||
<React.Fragment key={comment.comment_id}>
|
||||
<Comment
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
parentId={comment.parent_id || null}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
/>
|
||||
<CommentsReplies uri={uri} parentId={comment.comment_id} linkedComment={linkedComment} />
|
||||
</React.Fragment>
|
||||
<Comment
|
||||
isTopLevel
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
topLevelId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { makeSelectRepliesForParentId } from 'redux/selectors/comments';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import CommentsReplies from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
comments: makeSelectRepliesForParentId(props.parentId)(state),
|
||||
comments: makeSelectRepliesForParentId(props.topLevelId)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doToast,
|
||||
})(CommentsReplies);
|
||||
export default connect(select)(CommentsReplies);
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
// @flow
|
||||
import { SITE_NAME } from 'config';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Comment from 'component/comment';
|
||||
import Button from 'component/button';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
type Props = {
|
||||
comments: Array<any>,
|
||||
|
@ -14,18 +11,24 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
linkedComment?: Comment,
|
||||
parentId: string,
|
||||
topLevelId: string,
|
||||
commentingEnabled: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
topLevelIsReplying: boolean,
|
||||
setTopLevelIsReplying: boolean => void,
|
||||
};
|
||||
|
||||
function CommentsReplies(props: Props) {
|
||||
const { uri, comments, claimIsMine, myChannels, linkedComment, parentId, commentingEnabled, doToast } = props;
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
uri,
|
||||
comments,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
linkedComment,
|
||||
topLevelId,
|
||||
commentingEnabled,
|
||||
topLevelIsReplying,
|
||||
setTopLevelIsReplying,
|
||||
} = props;
|
||||
const [isExpanded, setExpanded] = React.useState(false);
|
||||
const [start, setStart] = React.useState(0);
|
||||
const [end, setEnd] = React.useState(9);
|
||||
|
@ -33,7 +36,6 @@ function CommentsReplies(props: Props) {
|
|||
const numberOfComments = comments ? comments.length : 0;
|
||||
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
||||
const commentsIndexOfLInked = comments && sortedComments.findIndex(e => e.comment_id === linkedCommentId);
|
||||
const hasChannels = myChannels && myChannels.length > 0;
|
||||
|
||||
function showMore() {
|
||||
if (start > 0) {
|
||||
|
@ -61,16 +63,7 @@ function CommentsReplies(props: Props) {
|
|||
setStart(numberOfComments || 0);
|
||||
}
|
||||
setEnd(numberOfComments + 1);
|
||||
setReplying(false);
|
||||
}
|
||||
|
||||
function handleCommentReply() {
|
||||
if (!hasChannels) {
|
||||
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
|
||||
doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) });
|
||||
} else {
|
||||
setReplying(!isReplying);
|
||||
}
|
||||
setTopLevelIsReplying(false);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -91,84 +84,71 @@ function CommentsReplies(props: Props) {
|
|||
const displayedComments = sortedComments.slice(start, end);
|
||||
|
||||
return (
|
||||
<li className="comment__replies-container">
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
(Boolean(numberOfComments) || topLevelIsReplying) && (
|
||||
<div className="comment__replies-container">
|
||||
{Boolean(numberOfComments) && (
|
||||
<Button
|
||||
className="comment__action"
|
||||
label={
|
||||
isExpanded
|
||||
? __('Hide %number% Replies', { number: numberOfComments })
|
||||
: __('Show %number% Replies', { number: numberOfComments })
|
||||
}
|
||||
onClick={() => setExpanded(!isExpanded)}
|
||||
icon={ICONS.DOWN}
|
||||
<div className="comment__actions--nested">
|
||||
<Button
|
||||
className="comment__action"
|
||||
label={
|
||||
isExpanded
|
||||
? __('Hide %number% Replies', { number: numberOfComments })
|
||||
: __('Show %number% Replies', { number: numberOfComments })
|
||||
}
|
||||
onClick={() => setExpanded(!isExpanded)}
|
||||
icon={isExpanded ? ICONS.UP : ICONS.DOWN}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{comments && displayedComments && isExpanded && (
|
||||
<div>
|
||||
<div className="comment__replies">
|
||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||
|
||||
<ul className="comments--replies">
|
||||
{displayedComments.map((comment, index) => {
|
||||
return (
|
||||
<Comment
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
key={comment.comment_id}
|
||||
message={comment.comment}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
commentingEnabled={commentingEnabled}
|
||||
hideReplyButton={index !== displayedComments.length - 1}
|
||||
topLevelIsReplying={topLevelIsReplying}
|
||||
setTopLevelIsReplying={setTopLevelIsReplying}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && comments && (end < numberOfComments || start > 0) && (
|
||||
<div className="comment__actions">
|
||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{topLevelIsReplying && (
|
||||
<CommentCreate
|
||||
isNested={isExpanded}
|
||||
key={topLevelId}
|
||||
uri={uri}
|
||||
topLevelId={topLevelId}
|
||||
onDoneReplying={() => handleCommentDone()}
|
||||
onCancelReplying={() => setTopLevelIsReplying(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{comments && displayedComments && isExpanded && (
|
||||
<div>
|
||||
<div className="comment__replies">
|
||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||
|
||||
<ul className="comments--replies">
|
||||
{displayedComments.map(comment => {
|
||||
return (
|
||||
<Comment
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
key={comment.comment_id}
|
||||
message={comment.comment}
|
||||
parentId={comment.parent_id || null}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{!isReplying && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action--nested"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && comments && (end < numberOfComments || start > 0) && (
|
||||
<div className="comment__actions">
|
||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isReplying ? (
|
||||
<CommentCreate
|
||||
isNested={isExpanded}
|
||||
key={parentId}
|
||||
uri={uri}
|
||||
parentId={parentId}
|
||||
onDoneReplying={() => handleCommentDone()}
|
||||
onCancelReplying={() => setReplying(false)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -832,4 +832,10 @@ export const icons = {
|
|||
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.UPVOTE]: buildIcon(
|
||||
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
||||
),
|
||||
[ICONS.DOWNVOTE]: buildIcon(
|
||||
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
|
||||
),
|
||||
};
|
||||
|
|
|
@ -117,3 +117,5 @@ export const NOTIFICATION = 'Bell';
|
|||
export const LAYOUT = 'Layout';
|
||||
export const REPLY = 'Reply';
|
||||
export const YOUTUBE = 'Youtube';
|
||||
export const UPVOTE = 'Upvote';
|
||||
export const DOWNVOTE = 'Downvote';
|
||||
|
|
2
ui/constants/reactions.js
Normal file
2
ui/constants/reactions.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const LIKE = 'like';
|
||||
export const DISLIKE = 'dislike';
|
|
@ -148,9 +148,8 @@ class FilePage extends React.Component<Props> {
|
|||
<FileDescription uri={uri} />
|
||||
<FileValues uri={uri} />
|
||||
<FileDetails uri={uri} />
|
||||
{/* <WaitUntilOnPage lastUpdateDate={this.lastReset} skipWait={Boolean(linkedComment)}> */}
|
||||
|
||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||
{/* </WaitUntilOnPage> */}
|
||||
</div>
|
||||
|
||||
<RecommendedContent uri={uri} />
|
||||
|
|
|
@ -208,6 +208,10 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
|||
|
||||
return comments
|
||||
? comments.filter(comment => {
|
||||
if (!comment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelClaim = claimsById[comment.channel_id];
|
||||
|
||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||
|
|
|
@ -8,6 +8,7 @@ $thumbnailWidthSmall: 2rem;
|
|||
}
|
||||
|
||||
.comments--replies {
|
||||
list-style-type: none;
|
||||
margin-left: var(--spacing-m);
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -25,7 +26,7 @@ $thumbnailWidthSmall: 2rem;
|
|||
|
||||
.comment {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-small);
|
||||
margin: 0;
|
||||
|
||||
|
@ -43,6 +44,11 @@ $thumbnailWidthSmall: 2rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.comment__replies-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -57,7 +63,7 @@ $thumbnailWidthSmall: 2rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__reply {
|
||||
.comment--reply {
|
||||
margin: 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
|
@ -100,7 +106,7 @@ $thumbnailWidthSmall: 2rem;
|
|||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment__highlighted {
|
||||
.comment--highlighted {
|
||||
background: var(--color-comment-highlighted);
|
||||
box-shadow: 0 0 0 var(--spacing-xs) var(--color-comment-highlighted);
|
||||
border-radius: 4px;
|
||||
|
@ -191,7 +197,6 @@ $thumbnailWidthSmall: 2rem;
|
|||
.comment__actions {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc(#{$thumbnailWidthSmall} + var(--spacing-xs));
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-xs);
|
||||
|
@ -204,10 +209,12 @@ $thumbnailWidthSmall: 2rem;
|
|||
.button__label {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
|
||||
}
|
||||
.comment__actions--nested {
|
||||
@extend .comment__actions;
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-l)));
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.comment__action {
|
||||
|
@ -216,6 +223,13 @@ $thumbnailWidthSmall: 2rem;
|
|||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.comment__action--active {
|
||||
.icon {
|
||||
fill: var(--color-primary-alt);
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__action--nested {
|
||||
@extend .comment__action;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue