Added the ability to reply to comments

This commit is contained in:
Kenneth Tubman V 2020-02-04 22:55:00 -05:00 committed by Sean Yesmunt
parent 1bb61a0bbf
commit 613ba98676
5 changed files with 151 additions and 7 deletions

View file

@ -915,11 +915,12 @@
"Support %amount% LBC": "Support %amount% LBC",
"You deposited %amount% LBC as a support!": "You deposited %amount% LBC as a support!",
"LBRY Link": "LBRY Link",
"Publish to %uri%": "Publish to %uri%",
"Your wallet": "Your wallet",
"Publish a file, or create a channel": "Publish a file, or create a channel",
"Your account": "Your account",
"Channel profile picture": "Channel profile picture",
"Comment Acknowledgement": "Comment Acknowledgement",
"A few things to know before making your comment:": "A few things to know before making your comment:",
"Commenting is in alpha. During the alpha, all comments are sent to a LBRY, Inc. server, not the LBRY network itself.": "Commenting is in alpha. During the alpha, all comments are sent to a LBRY, Inc. server, not the LBRY network itself.",
"Deleting or editing comments is not currently possible. Please be mindful of this when posting.": "Deleting or editing comments is not currently possible. Please be mindful of this when posting.",
"When the alpha ends, we will attempt to transition comments, but do not promise to do so.": "When the alpha ends, we will attempt to transition comments, but do not promise to do so.",
"Reply": "Reply",
"refreshing the app": "refreshing the app",
"Follower": "Follower",
"%repost_channel_link% reposted": "%repost_channel_link% reposted",

View file

@ -10,11 +10,15 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import { FormField, Form } from 'component/common/form';
import CommentReply from '../commentReply/index';
import classnames from 'classnames';
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
message: string, // comment body
timePosted: number, // Comment timestamp
channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail
@ -33,6 +37,7 @@ const ESCAPE_KEY = 27;
function Comment(props: Props) {
const {
uri,
author,
authorUri,
timePosted,
@ -44,6 +49,7 @@ function Comment(props: Props) {
channelIsBlocked,
commentIsMine,
commentId,
parentId,
updateComment,
deleteComment,
} = props;
@ -55,6 +61,9 @@ function Comment(props: Props) {
// used for controlling the visibility of the menu icon
const [mouseIsHovering, setMouseHover] = useState(false);
// used for controlling visibility of reply comment component
const [isReplying, setReplying] = useState(false);
// to debounce subsequent requests
const shouldFetch =
channel === undefined ||
@ -96,12 +105,17 @@ function Comment(props: Props) {
function handleSubmit() {
updateComment(commentId, editedMessage);
setEditing(false);
setReplying(false);
}
function handleDeleteComment() {
deleteComment(commentId);
}
function handleReply() {
setReplying(true);
}
function handleMouseOver() {
setMouseHover(true);
}
@ -111,7 +125,11 @@ function Comment(props: Props) {
}
return (
<li className="comment" onMouseOver={handleMouseOver} onMouseOut={handleMouseOut}>
<li
className={classnames('comment', { comment__reply: parentId !== null })}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
<div className="comment__author-thumbnail">
{authorUri ? <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small /> : <ChannelThumbnail small />}
</div>
@ -149,6 +167,13 @@ function Comment(props: Props) {
<MenuItem className="comment__menu-option" onSelect={handleDeleteComment}>
{__('Delete')}
</MenuItem>
{parentId === null ? (
<MenuItem className="comment__menu-option" onSelect={handleReply}>
{__('Reply')}
</MenuItem>
) : (
''
)}
</MenuList>
</Menu>
)}
@ -187,6 +212,7 @@ function Comment(props: Props) {
</div>
)}
</div>
<div>{isReplying ? <CommentReply uri={uri} parentId={commentId} setReplying={setReplying} /> : ''}</div>
</div>
</li>
);

View file

@ -0,0 +1,88 @@
// @flow
import { CHANNEL_NEW } from 'constants/claim';
import React, { useEffect, useState } from 'react';
import { FormField, Form } from 'component/common/form';
import Button from 'component/button';
import ChannelSection from 'component/selectChannel';
import usePersistedState from 'effects/use-persisted-state';
import * as MODALS from 'constants/modal_types';
import I18nMessage from '../i18nMessage/view';
type Props = {
commentingEnabled: boolean,
claim: StreamClaim,
parentId: string,
openModal: (id: string, { onCommentAcknowledge: () => void }) => void,
replyComment: (string, string, string, string) => void,
setReplying: boolean => void,
};
export function CommentReply(props: Props) {
const { commentingEnabled, replyComment, setReplying, claim, openModal, parentId } = props;
const { claim_id: claimId } = claim;
const [commentValue, setCommentValue] = usePersistedState(`comment-${claimId}`, '');
const [commentAck, setCommentAck] = usePersistedState('comment-acknowledge', false);
const [channel, setChannel] = usePersistedState('comment-channel', 'anonymous');
const [charCount, setCharCount] = useState(commentValue.length);
function handleCommentChange(event) {
setCommentValue(event.target.value);
}
function handleChannelChange(channel) {
setChannel(channel);
}
function handleCommentAck() {
setCommentAck(true);
}
function onTextareaFocus() {
if (!commentAck) {
openModal(MODALS.COMMENT_ACKNOWEDGEMENT, { onCommentAcknowledge: handleCommentAck });
}
}
function handleSubmit() {
if (channel !== CHANNEL_NEW && commentValue.length);
replyComment(commentValue, claimId, channel, parentId);
setCommentValue('');
}
useEffect(() => setCharCount(commentValue.length), [commentValue]);
if (!commentingEnabled) {
return (
<I18nMessage tokens={{ sign_in_link: <Button button="link" requiresAuth label={__('sign in')} /> }}>
Please %sign_in_link% to comment.
</I18nMessage>
);
}
return (
<Form className="comment__reply-form" onSubmit={handleSubmit}>
<ChannelSection channel={channel} onChannelChange={handleChannelChange} />
<FormField
disabled={channel === CHANNEL_NEW}
type="textarea"
name="content_description"
label={__('Comment')}
onFocus={onTextareaFocus}
placeholder={__('Say something about this...')}
value={commentValue}
charCount={charCount}
onChange={handleCommentChange}
/>
<div className="section__actions">
<Button
button="primary"
disabled={channel === CHANNEL_NEW || !commentValue.length}
type="submit"
label={__('Post')}
requiresAuth={IS_WEB}
/>
<Button button="link" label={__('Cancel')} onClick={() => setReplying(false)} />
</div>
</Form>
);
}

View file

@ -29,12 +29,31 @@ function CommentList(props: Props) {
fetchComments(uri);
}, [fetchComments, uri]);
function sortByParent(arrayOfComments) {
let parentComments = arrayOfComments.filter(comment => comment.parent_id === undefined);
let childComments = arrayOfComments.filter(comment => comment.parent_id !== undefined);
let sortedArray = [];
parentComments.forEach(parentComment => {
sortedArray.push(parentComment);
childComments
.filter(childComment => childComment.parent_id === parentComment.comment_id)
.forEach(childComment => {
sortedArray.push(childComment);
});
});
return sortedArray;
}
return (
<ul className="comments">
{comments &&
comments.map(comment => {
sortByParent(comments).map(comment => {
return (
<Comment
uri={uri}
authorUri={comment.channel_url}
author={comment.channel_name}
claimId={comment.claim_id}

View file

@ -19,6 +19,16 @@
}
}
.comment__reply {
border-left: 5px solid var(--color-border);
padding-left: var(--spacing-small);
}
.comment__reply-form {
border-top: 1px solid var(--color-border);
margin-top: var(--spacing-small);
}
.comment__body_container {
padding-right: var(--spacing-small);
flex: 1;