// @flow import React from 'react'; import classnames from 'classnames'; import Spinner from 'component/spinner'; import CommentCreate from 'component/commentCreate'; import LivestreamComment from 'component/livestreamComment'; import Button from 'component/button'; import UriIndicator from 'component/uriIndicator'; import CreditAmount from 'component/common/credit-amount'; import ChannelThumbnail from 'component/channelThumbnail'; import Tooltip from 'component/common/tooltip'; type Props = { uri: string, claim: ?StreamClaim, activeViewers: number, embed?: boolean, doCommentSocketConnect: (string, string) => void, doCommentSocketDisconnect: (string) => void, doCommentList: (string, string, number, number) => void, comments: Array<Comment>, fetchingComments: boolean, doSuperChatList: (string) => void, superChats: Array<Comment>, superChatsTotalAmount: number, myChannels: ?Array<ChannelClaim>, }; const VIEW_MODE_CHAT = 'view_chat'; const VIEW_MODE_SUPER_CHAT = 'view_superchat'; const COMMENT_SCROLL_OFFSET = 100; const COMMENT_SCROLL_TIMEOUT = 25; export default function LivestreamComments(props: Props) { const { claim, uri, embed, doCommentSocketConnect, doCommentSocketDisconnect, comments, doCommentList, fetchingComments, doSuperChatList, superChats, superChatsTotalAmount, myChannels, } = props; const commentsRef = React.createRef(); const [scrollBottom, setScrollBottom] = React.useState(true); const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT); const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false); const claimId = claim && claim.claim_id; const commentsLength = comments && comments.length; const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats; const discussionElement = document.querySelector('.livestream__comments'); const commentElement = document.querySelector('.livestream-comment'); // todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine function isMyComment(channelId: string) { if (myChannels != null && channelId != null) { for (let i = 0; i < myChannels.length; i++) { if (myChannels[i].claim_id === channelId) { return true; } } } return false; } React.useEffect(() => { if (claimId) { doCommentList(uri, '', 1, 75); doSuperChatList(uri); doCommentSocketConnect(uri, claimId); } return () => { if (claimId) { doCommentSocketDisconnect(claimId); } }; }, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]); const handleScroll = React.useCallback(() => { if (discussionElement) { const negativeCommentHeight = commentElement && -1 * commentElement.offsetHeight; const isAtRecent = negativeCommentHeight && discussionElement.scrollTop >= negativeCommentHeight; setScrollBottom(isAtRecent); } }, [commentElement, discussionElement]); React.useEffect(() => { if (discussionElement) { discussionElement.addEventListener('scroll', handleScroll); if (commentsLength > 0) { // Only update comment scroll if the user hasn't scrolled up to view old comments // If they have, do nothing if (!performedInitialScroll) { setTimeout( () => (discussionElement.scrollTop = discussionElement.scrollHeight - discussionElement.offsetHeight + COMMENT_SCROLL_OFFSET), COMMENT_SCROLL_TIMEOUT ); setPerformedInitialScroll(true); } } return () => discussionElement.removeEventListener('scroll', handleScroll); } }, [commentsLength, discussionElement, handleScroll, performedInitialScroll, setPerformedInitialScroll]); if (!claim) { return null; } function scrollBack() { if (discussionElement) { discussionElement.scrollTop = 0; setScrollBottom(true); } } return ( <div className="card livestream__discussion"> <div className="card__header--between livestream-discussion__header"> <div className="livestream-discussion__title">{__('Live discussion')}</div> {superChatsTotalAmount > 0 && ( <div className="recommended-content__toggles"> <Button className={classnames('button-toggle', { 'button-toggle--active': viewMode === VIEW_MODE_CHAT, })} label={__('Chat')} onClick={() => setViewMode(VIEW_MODE_CHAT)} /> <Button className={classnames('button-toggle', { 'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT, })} label={ <> <CreditAmount amount={superChatsTotalAmount} size={8} /> {__('Tipped')} </> } onClick={() => setViewMode(VIEW_MODE_SUPER_CHAT)} /> </div> )} </div> <> {fetchingComments && !comments && ( <div className="main--empty"> <Spinner /> </div> )} <div ref={commentsRef} className="livestream__comments-wrapper"> {viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && superChats && ( <div className="livestream-superchats__wrapper"> <div className="livestream-superchats__inner"> {superChats.map((superChat: Comment) => ( <Tooltip key={superChat.comment_id} label={superChat.comment}> <div className="livestream-superchat"> <div className="livestream-superchat__thumbnail"> <ChannelThumbnail uri={superChat.channel_url} xsmall /> </div> <div className="livestream-superchat__info"> <UriIndicator uri={superChat.channel_url} link /> <CreditAmount size={10} className="livestream-superchat__amount-large" amount={superChat.support_amount} isFiat={superChat.is_fiat} /> </div> </div> </Tooltip> ))} </div> </div> )} {!fetchingComments && comments.length > 0 ? ( <div className="livestream__comments"> {commentsToDisplay.map((comment) => ( <LivestreamComment key={comment.comment_id} uri={uri} authorUri={comment.channel_url} commentId={comment.comment_id} message={comment.comment} supportAmount={comment.support_amount} isFiat={comment.is_fiat} commentIsMine={comment.channel_id && isMyComment(comment.channel_id)} /> ))} </div> ) : ( <div className="main--empty" style={{ flex: 1 }} /> )} {!scrollBottom && ( <Button button="alt" className="livestream__comments-scroll__down" label={__('Recent Comments')} onClick={scrollBack} /> )} <div className="livestream__comment-create"> <CommentCreate livestream bottom embed={embed} uri={uri} /> </div> </div> </> </div> ); }