// @flow import 'scss/component/_livestream-chat.scss'; // $FlowFixMe import { grey } from '@mui/material/colors'; import { useIsMobile } from 'effects/use-screensize'; import * as ICONS from 'constants/icons'; import Button from 'component/button'; import classnames from 'classnames'; import CommentCreate from 'component/commentCreate'; import CreditAmount from 'component/common/credit-amount'; import LivestreamComment from 'component/livestreamComment'; import LivestreamComments from 'component/livestreamComments'; import LivestreamSuperchats from './livestream-superchats'; import LivestreamMenu from './livestream-menu'; import React from 'react'; import Yrbl from 'component/yrbl'; import { getTipValues } from 'util/livestream'; import Slide from '@mui/material/Slide'; import useGetUserMemberships from 'effects/use-get-user-memberships'; export const VIEW_MODES = { CHAT: 'chat', SUPERCHAT: 'sc', }; const COMMENT_SCROLL_TIMEOUT = 25; type Props = { embed?: boolean, isPopoutWindow?: boolean, uri: string, hideHeader?: boolean, superchatsHidden?: boolean, customViewMode?: string, setCustomViewMode?: (any) => void, // redux claimId?: string, comments: Array, pinnedComments: Array, superChats: Array, theme: string, doCommentList: ( uri: string, parentId: ?string, page: number, pageSize: number, sortBy: ?number, isLivestream: boolean ) => void, doResolveUris: (uris: Array, cache: boolean) => void, doSuperChatList: (uri: string) => void, claimsByUri: { [string]: any }, doFetchUserMemberships: (claimIdCsv: string) => void, setLayountRendered: (boolean) => void, }; export default function LivestreamChatLayout(props: Props) { const { claimId, comments: commentsByChronologicalOrder, embed, isPopoutWindow, pinnedComments, superChats: superChatsByAmount, uri, hideHeader, superchatsHidden, customViewMode, theme, setCustomViewMode, doCommentList, doResolveUris, doSuperChatList, doFetchUserMemberships, claimsByUri, setLayountRendered, } = props; const isMobile = useIsMobile() && !isPopoutWindow; const webElement = document.querySelector('.livestream__comments'); const mobileElement = document.querySelector('.livestream__comments--mobile'); const discussionElement = isMobile ? mobileElement : webElement; const allCommentsElem = document.querySelectorAll('.livestream__comment'); const lastCommentElem = allCommentsElem && allCommentsElem[allCommentsElem.length - 1]; const [viewMode, setViewMode] = React.useState(VIEW_MODES.CHAT); const [scrollPos, setScrollPos] = React.useState(0); const [showPinned, setShowPinned] = React.useState(true); const [resolvingSuperChats, setResolvingSuperChats] = React.useState(false); const [openedPopoutWindow, setPopoutWindow] = React.useState(undefined); const [chatHidden, setChatHidden] = React.useState(false); const [didInitialScroll, setDidInitialScroll] = React.useState(false); const [minScrollHeight, setMinScrollHeight] = React.useState(0); const [keyboardOpened, setKeyboardOpened] = React.useState(false); const [superchatsAmount, setSuperchatsAmount] = React.useState(false); const [chatElement, setChatElement] = React.useState(); let superChatsByChronologicalOrder = []; if (superChatsByAmount) superChatsByAmount.forEach((chat) => superChatsByChronologicalOrder.push(chat)); if (superChatsByChronologicalOrder.length > 0) { superChatsByChronologicalOrder.sort((a, b) => b.timestamp - a.timestamp); } // get commenter claim ids for checking premium status const commenterClaimIds = commentsByChronologicalOrder.map((comment) => { return comment.channel_id; }); // update premium status const shouldFetchUserMemberships = true; useGetUserMemberships( shouldFetchUserMemberships, commenterClaimIds, claimsByUri, doFetchUserMemberships, [commentsByChronologicalOrder], true ); const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByChronologicalOrder; const commentsLength = commentsToDisplay && commentsToDisplay.length; const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null; const { superChatsChannelUrls, superChatsFiatAmount, superChatsLBCAmount } = getTipValues( superChatsByChronologicalOrder ); const scrolledPastRecent = Boolean( (viewMode !== VIEW_MODES.SUPERCHAT || !resolvingSuperChats) && (!isMobile ? scrollPos < 0 : scrollPos < minScrollHeight) ); const restoreScrollPos = React.useCallback(() => { if (discussionElement) { discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight; if (isMobile) { const pos = lastCommentElem && discussionElement.scrollTop - lastCommentElem.getBoundingClientRect().height; if (!minScrollHeight || minScrollHeight !== pos) { setMinScrollHeight(pos); } } } }, [discussionElement, isMobile, lastCommentElem, minScrollHeight]); function toggleClick(toggleMode: string) { if (toggleMode === VIEW_MODES.SUPERCHAT) { toggleSuperChat(); } else { setViewMode(VIEW_MODES.CHAT); } if (discussionElement) { discussionElement.scrollTop = 0; } } function toggleSuperChat() { const hasNewSuperchats = !superchatsAmount || superChatsChannelUrls.length !== superchatsAmount; if (superChatsChannelUrls && hasNewSuperchats) { setSuperchatsAmount(superChatsChannelUrls.length); doResolveUris(superChatsChannelUrls, false); } setViewMode(VIEW_MODES.SUPERCHAT); if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT); } React.useEffect(() => { if (setLayountRendered) setLayountRendered(true); }, [setLayountRendered]); React.useEffect(() => { if (customViewMode && customViewMode !== viewMode) { setViewMode(customViewMode); } }, [customViewMode, viewMode]); React.useEffect(() => { if (claimId) { doCommentList(uri, undefined, 1, 75, undefined, true); doSuperChatList(uri); } }, [claimId, uri, doCommentList, doSuperChatList]); React.useEffect(() => { if (isMobile && !didInitialScroll) { restoreScrollPos(); setDidInitialScroll(true); } }, [didInitialScroll, isMobile, restoreScrollPos, viewMode]); React.useEffect(() => { if (discussionElement && !openedPopoutWindow) setChatElement(discussionElement); }, [discussionElement, openedPopoutWindow]); // Register scroll handler (TODO: Should throttle/debounce) React.useEffect(() => { function handleScroll() { if (chatElement) { const scrollTop = chatElement.scrollTop; if (scrollTop !== scrollPos) { setScrollPos(scrollTop); } } } if (chatElement) { chatElement.addEventListener('scroll', handleScroll); return () => chatElement.removeEventListener('scroll', handleScroll); } }, [chatElement, scrollPos]); // Retain scrollPos=0 when receiving new messages. React.useEffect(() => { if (discussionElement && commentsLength > 0) { // Only update comment scroll if the user hasn't scrolled up to view old comments // $FlowFixMe if (scrollPos && (!isMobile || minScrollHeight) && scrollPos >= minScrollHeight) { // +ve scrollPos: not scrolled (Usually, there'll be a few pixels beyond 0). // -ve scrollPos: user scrolled. const timer = setTimeout(() => { // Use a timer here to ensure we reset after the new comment has been rendered. if (!isMobile) { discussionElement.scrollTop = 0; } else { restoreScrollPos(); } }, COMMENT_SCROLL_TIMEOUT); return () => clearTimeout(timer); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [commentsLength]); // (Just respond to 'commentsLength' updates and nothing else) // Restore Scroll Pos after mobile input opens keyboard and avoid scroll height conflicts React.useEffect(() => { if (keyboardOpened) { const timer = setTimeout(() => { restoreScrollPos(); setKeyboardOpened(false); }, 300); return () => clearTimeout(timer); } }, [keyboardOpened, restoreScrollPos]); if (!claimId) return null; if (openedPopoutWindow || chatHidden) { return (
{__('Live Chat')}
{openedPopoutWindow && (
} />
); } const toggleProps = { viewMode, onClick: (toggleMode) => toggleClick(toggleMode) }; return (
{!hideHeader && (
{__('Live Chat')} setChatHidden(true)} setPopoutWindow={(v) => setPopoutWindow(v)} isMobile={isMobile} />
{superChatsByChronologicalOrder && (
{/* the superchats in chronological order button */} {/* the button to show superchats listed by most to least support amount */} {__('Tipped')} } />
)}
)}
{isMobile && ((pinnedComment && showPinned) || (superChatsByAmount && !superchatsHidden)) && ( )} {viewMode === VIEW_MODES.CHAT && superChatsByAmount && ( )} {pinnedComment && viewMode === VIEW_MODES.CHAT && (isMobile ? (
setShowPinned(false)} isMobile setResolvingSuperChats={setResolvingSuperChats} />
) : ( showPinned && (
) ))}
{scrolledPastRecent && (
); } type ToggleProps = { viewMode: string, toggleMode: string, label: string | any, onClick: (string) => void, }; const ChatContentToggle = (props: ToggleProps) => { const { viewMode, toggleMode, label, onClick } = props; return (