diff --git a/ui/component/comment/index.js b/ui/component/comment/index.js index a78075db5..69f69122e 100644 --- a/ui/component/comment/index.js +++ b/ui/component/comment/index.js @@ -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); diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index dcd6cb700..4b5e395ab 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -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, + 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 (
  • setMouseHover(true)} onMouseOut={() => setMouseHover(false)} > -
    - {authorUri ? : } -
    - -
    -
    -
    - {!author ? ( - {__('Anonymous')} - ) : ( -
    -
    - - - - - - {commentIsMine ? ( - <> - setEditing(true)}> - {__('Edit')} - - deleteComment(commentId)}> - {__('Delete')} - - - ) : ( - blockChannel(authorUri)}> - {__('Block Channel')} - - )} - - -
    -
    -
    - {isEditing ? ( -
    - -
    -
    - - ) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( -
    - {/* */} - - {/* */} -
    +
    +
    + {authorUri ? ( + ) : ( -
    - -
    + )}
    + +
    +
    +
    + {!author ? ( + {__('Anonymous')} + ) : ( +
    +
    + + + + + + {commentIsMine ? ( + <> + setEditing(true)}> + {__('Edit')} + + deleteComment(commentId)}> + {__('Delete')} + + + ) : ( + blockChannel(authorUri)}> + {__('Block Channel')} + + )} + + +
    +
    +
    + {isEditing ? ( +
    + +
    +
    + + ) : ( + <> +
    + {editedMessage.length >= LENGTH_TO_COLLAPSE ? ( + + + + ) : ( + + )} +
    + +
    + + {!hideReplyButton && ( +
    + + )} +
    +
    + + {isTopLevel && ( + + )}
  • ); } diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index ca5705779..3b417a8fd 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -18,17 +18,17 @@ type Props = { openModal: (id: string, { onCommentAcknowledge: () => void }) => void, createComment: (string, string, string, ?string) => void, channels: ?Array, - 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(); } diff --git a/ui/component/commentReactions/index.js b/ui/component/commentReactions/index.js new file mode 100644 index 000000000..baddf0e25 --- /dev/null +++ b/ui/component/commentReactions/index.js @@ -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); diff --git a/ui/component/commentReactions/view.jsx b/ui/component/commentReactions/view.jsx new file mode 100644 index 000000000..716832b0a --- /dev/null +++ b/ui/component/commentReactions/view.jsx @@ -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 ( + <> +