diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4f69c63..8e9283056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/ui/component/comment/index.js b/ui/component/comment/index.js index f3141821a..5fccbab35 100644 --- a/ui/component/comment/index.js +++ b/ui/component/comment/index.js @@ -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); diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 22e0ae2f3..d964df212 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -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, 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 ( -