Adds the ability for users to update & delete comments #3453

Merged
osilkin98 merged 2 commits from comment-options into master 2020-01-31 22:30:52 +01:00
11 changed files with 202 additions and 43 deletions

View file

@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Adds the ability for users to update & delete comments ([#3453](https://github.com/lbryio/lbry-desktop/pull/3453))
- New homepage design ([#3508](https://github.com/lbryio/lbry-desktop/pull/3508))
- Revamped invites system ([#3462](https://github.com/lbryio/lbry-desktop/pull/3462))
- Appimage support ([#3497](https://github.com/lbryio/lbry-desktop/pull/3497))

View file

@ -6,13 +6,14 @@ import {
makeSelectThumbnailForUri,
makeSelectIsUriResolving,
selectChannelIsBlocked,
doCommentUpdate, // doEditComment would be a more fitting name
doCommentAbandon,
} from 'lbry-redux';
import Comment from './view';
const select = (state, props) => ({
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
claim: props.authorUri && makeSelectClaimForUri(props.authorUri)(state),
channel: props.authorUri && makeSelectClaimForUri(props.authorUri)(state),
isResolvingUri: props.authorUri && makeSelectIsUriResolving(props.authorUri)(state),
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
@ -20,9 +21,8 @@ const select = (state, props) => ({
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
deleteComment: commentId => dispatch(doCommentAbandon(commentId)),
});
export default connect(
select,
perform
)(Comment);
export default connect(select, perform)(Comment);

View file

@ -1,25 +1,35 @@
// @flow
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { isEmpty } from 'util/object';
import relativeDate from 'tiny-relative-date';
import Button from 'component/button';
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';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import { FormField, Form } from 'component/common/form';
type Props = {
author: string,
authorUri: string,
message: string,
timePosted: number,
claim: ?Claim,
author: ?string, // LBRY Channel Name, e.g. @channel
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
commentId: string, // sha256 digest identifying the comment
message: string, // comment body
timePosted: number, // Comment timestamp
channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail
pending?: boolean,
resolveUri: string => void,
isResolvingUri: boolean,
channelIsBlocked: boolean,
resolveUri: string => void, // resolves the URI
isResolvingUri: boolean, // if the URI is currently being resolved
channelIsBlocked: boolean, // if the channel is blacklisted in the app
claimIsMine: boolean, // if you control the claim which this comment was posted on
commentIsMine: boolean, // if this comment was signed by an owned channel
updateComment: (string, string) => void,
deleteComment: string => void,
};
const LENGTH_TO_COLLAPSE = 300;
const ESCAPE_KEY = 27;
function Comment(props: Props) {
const {
@ -28,46 +38,144 @@ function Comment(props: Props) {
timePosted,
message,
neb-b commented 2020-01-23 22:05:12 +01:00 (Migrated from github.com)
Review

You are already wrapping this in commentIsMine so these aren't needed

You are already wrapping this in `commentIsMine` so these aren't needed
neb-b commented 2020-01-23 22:06:07 +01:00 (Migrated from github.com)
Review

set a classname based on if it's hovering, then we can use css variables. Currently in dark mode, the dots get darker when you hover.

set a classname based on if it's hovering, then we can use css variables. Currently in dark mode, the dots get darker when you hover.
neb-b commented 2020-01-23 22:06:34 +01:00 (Migrated from github.com)
Review

The space in between { and commentIsMine is because you disabled the precommit hook which formats the code.

The space in between `{` and `commentIsMine` is because you disabled the precommit hook which formats the code.
neb-b commented 2020-01-23 22:07:06 +01:00 (Migrated from github.com)
Review

these ^ comments aren't needed

these ^ comments aren't needed
osilkin98 commented 2020-01-25 23:30:51 +01:00 (Migrated from github.com)
Review

done

done
pending,
claim,
channel,
isResolvingUri,
resolveUri,
channelIsBlocked,
commentIsMine,
commentId,
updateComment,
deleteComment,
} = props;
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);
// to debounce subsequent requests
const shouldFetch =
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
channel === undefined ||
(channel !== null && channel.value_type === 'channel' && isEmpty(channel.meta) && !pending);
useEffect(() => {
// If author was extracted from the URI, then it must be valid.
if (authorUri && author && !isResolvingUri && shouldFetch) {
resolveUri(authorUri);
}
}, [isResolvingUri, shouldFetch, author, authorUri, resolveUri]);
if (isEditing) {
setCharCount(editedMessage.length);
// a user will try and press the escape key to cancel editing their comment
const handleEscape = event => {
if (event.keyCode === ESCAPE_KEY) {
setEditing(false);
}
};
window.addEventListener('keydown', handleEscape);
// removes the listener so it doesn't cause problems elsewhere in the app
return () => {
window.removeEventListener('keydown', handleEscape);
};
}
}, [isResolvingUri, shouldFetch, author, authorUri, resolveUri, editedMessage, isEditing, setEditing]);
function handleSetEditing() {
setEditing(true);
}
function handleEditMessageChanged(event) {
setCommentValue(event.target.value);
}
function handleSubmit() {
updateComment(commentId, editedMessage);
setEditing(false);
}
function handleDeleteComment() {
deleteComment(commentId);
}
function handleMouseOver() {
setMouseHover(true);
}
function handleMouseOut() {
setMouseHover(false);
}
return (
<li className="comment">
<li className="comment" onMouseOver={handleMouseOver} onMouseOut={handleMouseOut}>
<div className="comment__author-thumbnail">
{authorUri ? <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small /> : <ChannelThumbnail small />}
</div>
<div className="comment__body_container">
<span className="comment__meta">
{!author ? (
<span className="comment__author">{__('Anonymous')}</span>
) : (
<Button
className="button--uri-indicator truncated-text comment__author"
navigate={authorUri}
label={author}
/>
)}
<time className="comment__time" dateTime={timePosted}>
{relativeDate(timePosted)}
</time>
</span>
<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}
/>
)}
<time className="comment__time" dateTime={timePosted}>
{relativeDate(timePosted)}
</time>
</div>
<div className="comment__menu">
{commentIsMine && (
<Menu>
<MenuButton>
<Icon
size={18}
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
icon={ICONS.MORE_VERTICAL}
/>
</MenuButton>
<MenuList className="comment__menu-list">
<MenuItem className="comment__menu-option" onSelect={handleSetEditing}>
{__('Edit')}
</MenuItem>
<MenuItem className="comment__menu-option" onSelect={handleDeleteComment}>
{__('Delete')}
</MenuItem>
neb-b commented 2020-01-23 22:02:45 +01:00 (Migrated from github.com)
Review

This can just delete the comment, we don't need a modal for it.

This can just delete the comment, we don't need a modal for it.
</MenuList>
</Menu>
)}
</div>
</div>
<div>
{message.length >= LENGTH_TO_COLLAPSE ? (
{isEditing ? (
<Form onSubmit={handleSubmit}>
<FormField
type="textarea"
name="editing_comment"
value={editedMessage}
charCount={charCount}
onChange={handleEditMessageChanged}
/>
<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} />

View file

@ -1,16 +1,15 @@
import { connect } from 'react-redux';
import { makeSelectCommentsForUri, doCommentList } from 'lbry-redux';
import { makeSelectCommentsForUri, doCommentList, makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
import CommentsList from './view';
const select = (state, props) => ({
myChannels: selectMyChannelClaims(state),
comments: makeSelectCommentsForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
});
const perform = dispatch => ({
fetchComments: uri => dispatch(doCommentList(uri)),
});
export default connect(
select,
perform
)(CommentsList);
export default connect(select, perform)(CommentsList);

View file

@ -6,10 +6,25 @@ type Props = {
comments: Array<any>,
fetchComments: string => void,
uri: string,
claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>,
};
function CommentList(props: Props) {
const { fetchComments, uri, comments } = props;
const { fetchComments, uri, comments, claimIsMine, myChannels } = props;
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
const isMyComment = (channelId: string) => {
if (myChannels != null && channelId != null) {
neb-b commented 2020-01-23 22:04:30 +01:00 (Migrated from github.com)
Review

This was causing an error when I refreshed on a page with comments

"Cannot read property 'length' of undefined
    at isMyComment"
This was causing an error when I refreshed on a page with comments ``` "Cannot read property 'length' of undefined at isMyComment" ```
osilkin98 commented 2020-01-25 22:11:04 +01:00 (Migrated from github.com)
Review

Was using myChannels !== null instead of myChannels != null

Was using `myChannels !== null` instead of `myChannels != null`
for (let i = 0; i < myChannels.length; i++) {
if (myChannels[i].claim_id === channelId) {
return true;
}
}
}
return false;
};
useEffect(() => {
fetchComments(uri);
}, [fetchComments, uri]);
@ -22,12 +37,14 @@ function CommentList(props: Props) {
<Comment
authorUri={comment.channel_url}
author={comment.channel_name}
claimId={comment.channel_id}
claimId={comment.claim_id}
commentId={comment.comment_id}
key={comment.channel_id + 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)}
/>
);
})}

View file

@ -400,4 +400,11 @@ export const icons = {
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
</g>
),
[ICONS.MORE_VERTICAL]: buildIcon(
<g>
<circle cx="12" cy="5" r="1" />
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="19" r="1" />
</g>
),
};

View file

@ -46,7 +46,7 @@ class IconComponent extends React.PureComponent<Props> {
case 'blue':
return BLUE_COLOR;
default:
return undefined;
return color;
}
};

View file

@ -81,6 +81,7 @@ export const SIGN_IN = 'SignIn';
export const TRENDING = 'Trending';
neb-b commented 2020-01-23 22:03:50 +01:00 (Migrated from github.com)
Review

remove this one since we aren't using it

remove this one since we aren't using it
export const TOP = 'Top';
export const NEW = 'New';
export const MORE_VERTICAL = 'MoreVertical';
export const IMAGE = 'Image';
export const AUDIO = 'HeadPhones';
export const VIDEO = 'Video';

View file

@ -21,9 +21,12 @@
.comment__body_container {
padding-right: var(--spacing-small);
flex: 1;
}
.comment__meta {
display: flex;
justify-content: space-between;
text-overflow: ellipsis;
margin-bottom: var(--spacing-small);
}
@ -43,7 +46,26 @@
white-space: nowrap;
}
.comment__menu {
align-self: flex-end;
}
.comment__char-count {
align-self: flex-end;
font-size: var(--font-small);
}
.comment__menu-option {
display: flex;
align-items: center;
padding: var(--spacing-small);
font-size: var(--font-small);
}
.comment__menu-icon--hovering {
stroke: var(--color-comment-menu-hovering);
}
.comment__menu-icon {
stroke: var(--color-comment-menu);
}

View file

@ -36,6 +36,8 @@
--color-tabs-background: #434b53;
--color-tab-divider: var(--color-white);
--color-modal-background: var(--color-header-background);
--color-comment-menu: #6a6a6a;
--color-comment-menu-hovering: #e0e0e0;
// Text
--color-text: #eeeeee;

View file

@ -12,6 +12,8 @@
--color-background-overlay: #21252980;
--color-nag: #f26522;
--color-error: #fcafca;
--color-comment-menu: #e0e0e0;
--color-comment-menu-hovering: #6a6a6a;
// Text
--color-text-selection-bg: var(--color-secondary-alt);