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", "Support %amount% LBC": "Support %amount% LBC",
"You deposited %amount% LBC as a support!": "You deposited %amount% LBC as a support!", "You deposited %amount% LBC as a support!": "You deposited %amount% LBC as a support!",
"LBRY Link": "LBRY Link", "LBRY Link": "LBRY Link",
"Publish to %uri%": "Publish to %uri%", "Comment Acknowledgement": "Comment Acknowledgement",
"Your wallet": "Your wallet", "A few things to know before making your comment:": "A few things to know before making your comment:",
"Publish a file, or create a channel": "Publish a file, or create a channel", "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.",
"Your account": "Your account", "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.",
"Channel profile picture": "Channel profile picture", "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", "refreshing the app": "refreshing the app",
"Follower": "Follower", "Follower": "Follower",
"%repost_channel_link% reposted": "%repost_channel_link% reposted", "%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 Icon from 'component/common/icon';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import CommentReply from '../commentReply/index';
import classnames from 'classnames';
type Props = { type Props = {
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
parentId: 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
@ -33,6 +37,7 @@ const ESCAPE_KEY = 27;
function Comment(props: Props) { function Comment(props: Props) {
const { const {
uri,
author, author,
authorUri, authorUri,
timePosted, timePosted,
@ -44,6 +49,7 @@ function Comment(props: Props) {
channelIsBlocked, channelIsBlocked,
commentIsMine, commentIsMine,
commentId, commentId,
parentId,
updateComment, updateComment,
deleteComment, deleteComment,
} = props; } = props;
@ -55,6 +61,9 @@ function Comment(props: Props) {
// used for controlling the visibility of the menu icon // used for controlling the visibility of the menu icon
const [mouseIsHovering, setMouseHover] = useState(false); const [mouseIsHovering, setMouseHover] = useState(false);
// used for controlling visibility of reply comment component
const [isReplying, setReplying] = useState(false);
// to debounce subsequent requests // to debounce subsequent requests
const shouldFetch = const shouldFetch =
channel === undefined || channel === undefined ||
@ -96,12 +105,17 @@ function Comment(props: Props) {
function handleSubmit() { function handleSubmit() {
updateComment(commentId, editedMessage); updateComment(commentId, editedMessage);
setEditing(false); setEditing(false);
setReplying(false);
} }
function handleDeleteComment() { function handleDeleteComment() {
deleteComment(commentId); deleteComment(commentId);
} }
function handleReply() {
setReplying(true);
}
function handleMouseOver() { function handleMouseOver() {
setMouseHover(true); setMouseHover(true);
} }
@ -111,7 +125,11 @@ function Comment(props: Props) {
} }
return ( 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"> <div className="comment__author-thumbnail">
{authorUri ? <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small /> : <ChannelThumbnail small />} {authorUri ? <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small /> : <ChannelThumbnail small />}
</div> </div>
@ -149,6 +167,13 @@ function Comment(props: Props) {
<MenuItem className="comment__menu-option" onSelect={handleDeleteComment}> <MenuItem className="comment__menu-option" onSelect={handleDeleteComment}>
{__('Delete')} {__('Delete')}
</MenuItem> </MenuItem>
{parentId === null ? (
<MenuItem className="comment__menu-option" onSelect={handleReply}>
{__('Reply')}
</MenuItem>
) : (
''
)}
</MenuList> </MenuList>
</Menu> </Menu>
)} )}
@ -187,6 +212,7 @@ function Comment(props: Props) {
</div> </div>
)} )}
</div> </div>
<div>{isReplying ? <CommentReply uri={uri} parentId={commentId} setReplying={setReplying} /> : ''}</div>
</div> </div>
</li> </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);
}, [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 ( return (
<ul className="comments"> <ul className="comments">
{comments && {comments &&
comments.map(comment => { sortByParent(comments).map(comment => {
return ( return (
<Comment <Comment
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}

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 { .comment__body_container {
padding-right: var(--spacing-small); padding-right: var(--spacing-small);
flex: 1; flex: 1;