// @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import * as KEYCODES from 'constants/keycodes'; import { COMMENT_HIGHLIGHTED } from 'constants/classnames'; import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config'; import React, { useEffect, useState } from 'react'; import { parseURI } from 'util/lbryURI'; import DateTime from 'component/dateTime'; import Button from 'component/button'; import Expandable from 'component/expandable'; import MarkdownPreview from 'component/common/markdown-preview'; import Tooltip from 'component/common/tooltip'; import ChannelThumbnail from 'component/channelThumbnail'; import { Menu, MenuButton } from '@reach/menu-button'; 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'; import CommentCreate from 'component/commentCreate'; import CommentMenuList from 'component/commentMenuList'; import UriIndicator from 'component/uriIndicator'; import CreditAmount from 'component/common/credit-amount'; const AUTO_EXPAND_ALL_REPLIES = false; type Props = { clearPlayingUri: () => void, uri: string, claim: StreamClaim, 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 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, fetchReplies: (string, string, number, number, number) => void, totalReplyPages: number, commentModBlock: (string) => void, linkedCommentId?: string, linkedCommentAncestors: { [string]: Array }, myChannels: ?Array, commentingEnabled: boolean, doToast: ({ message: string }) => void, isTopLevel?: boolean, threadDepth: number, hideActions?: boolean, isPinned: boolean, othersReacts: ?{ like: number, dislike: number, }, commentIdentityChannel: any, activeChannelClaim: ?ChannelClaim, playingUri: ?PlayingUri, stakedLevel: number, supportAmount: number, numDirectReplies: number, isModerator: boolean, isGlobalMod: boolean, isFiat: boolean, supportDisabled: boolean, setQuickReply: (any) => void, quickReply: any, }; const LENGTH_TO_COLLAPSE = 300; function Comment(props: Props) { const { clearPlayingUri, claim, uri, author, authorUri, timePosted, message, channelIsBlocked, commentIsMine, commentId, updateComment, fetchReplies, totalReplyPages, linkedCommentId, linkedCommentAncestors, commentingEnabled, myChannels, doToast, isTopLevel, threadDepth, hideActions, isPinned, othersReacts, playingUri, stakedLevel, supportAmount, numDirectReplies, isModerator, isGlobalMod, isFiat, supportDisabled, setQuickReply, quickReply, } = props; const { push, replace, location: { pathname, search }, } = useHistory(); const isLinkedComment = linkedCommentId && linkedCommentId === commentId; const isInLinkedCommentChain = linkedCommentId && linkedCommentAncestors[linkedCommentId] && linkedCommentAncestors[linkedCommentId].includes(commentId); const showRepliesOnMount = isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES; const [isReplying, setReplying] = React.useState(false); const [isEditing, setEditing] = useState(false); const [editedMessage, setCommentValue] = useState(message); const [charCount, setCharCount] = useState(editedMessage.length); const [showReplies, setShowReplies] = useState(showRepliesOnMount); const [page, setPage] = useState(showRepliesOnMount ? 1 : 0); const [advancedEditor] = usePersistedState('comment-editor-mode', false); const [displayDeadComment, setDisplayDeadComment] = React.useState(false); const hasChannels = myChannels && myChannels.length > 0; const likesCount = (othersReacts && othersReacts.like) || 0; const dislikesCount = (othersReacts && othersReacts.dislike) || 0; const totalLikesAndDislikes = likesCount + dislikesCount; const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8; const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri; let channelOwnerOfContent; try { const { channelName } = parseURI(uri); if (channelName) { channelOwnerOfContent = channelName; } } catch (e) {} useEffect(() => { 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 === KEYCODES.ESCAPE) { setEditing(false); } }; window.addEventListener('keydown', handleEscape); // removes the listener so it doesn't cause problems elsewhere in the app return () => { window.removeEventListener('keydown', handleEscape); }; } }, [author, authorUri, editedMessage, isEditing, setEditing]); useEffect(() => { if (page > 0) { fetchReplies(uri, commentId, page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST); } }, [page, uri, commentId, fetchReplies]); function handleEditMessageChanged(event) { setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value); } function handleEditComment() { if (playingUri && playingUri.source === 'comment') { clearPlayingUri(); } setEditing(true); } function handleSubmit() { updateComment(commentId, editedMessage); if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage }); 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 { setReplying(!isReplying); } } function handleTimeClick() { const urlParams = new URLSearchParams(search); urlParams.delete('lc'); urlParams.append('lc', commentId); replace(`${pathname}?${urlParams.toString()}`); } const linkedCommentRef = React.useCallback((node) => { if (node !== null && window.pendingLinkedCommentScroll) { const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height delete window.pendingLinkedCommentScroll; window.scrollTo({ top: node.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT, left: 0, behavior: 'smooth', }); } }, []); return (
  • 0, })} id={commentId} >
    {authorUri ? ( ) : ( )}
    {isGlobalMod && ( )} {isModerator && ( )} {!author ? ( {__('Anonymous')} ) : ( )}
    {isEditing ? (
    ) : ( <>
    {slimedToDeath && !displayDeadComment ? (
    setDisplayDeadComment(true)} className="comment__dead"> {__('This comment was slimed to death.')}
    ) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( ) : ( )}
    {!hideActions && (
    {threadDepth !== 0 && (
    )} {numDirectReplies > 0 && !showReplies && (
    )} {numDirectReplies > 0 && showReplies && (
    )} {isReplying && ( { setShowReplies(true); setReplying(false); }} onCancelReplying={() => { setReplying(false); }} supportDisabled={supportDisabled} /> )} )}
    {showReplies && ( setPage(page + 1)} hasMore={page < totalReplyPages} /> )}
  • ); } export default Comment;