// @flow import 'scss/component/_livestream-chat.scss'; // $FlowFixMe import { Global } from '@emotion/react'; // $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 Spinner from 'component/spinner'; import Yrbl from 'component/yrbl'; import { getTipValues } from 'util/livestream'; import Slide from '@mui/material/Slide'; const VIEW_MODES = { CHAT: 'chat', SUPERCHAT: 'sc', }; const COMMENT_SCROLL_TIMEOUT = 25; const LARGE_SUPER_CHAT_LIST_THRESHOLD = 20; type Props = { claim: ?StreamClaim, comments: Array, embed?: boolean, isPopoutWindow?: boolean, pinnedComments: Array, superChats: Array, uri: string, hideHeader?: boolean, superchatsHidden?: boolean, customViewMode?: string, theme: string, setCustomViewMode?: (any) => void, doCommentList: (string, string, number, number) => void, doResolveUris: (Array, boolean) => void, doSuperChatList: (string) => void, }; export default function LivestreamChatLayout(props: Props) { const { claim, comments: commentsByChronologicalOrder, embed, isPopoutWindow, pinnedComments, superChats: superChatsByAmount, uri, hideHeader, superchatsHidden, customViewMode, theme, setCustomViewMode, doCommentList, doResolveUris, doSuperChatList, } = 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 claimId = claim && claim.claim_id; const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByAmount; const commentsLength = commentsToDisplay && commentsToDisplay.length; const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null; const { superChatsChannelUrls, superChatsFiatAmount, superChatsLBCAmount } = getTipValues(superChatsByAmount); const scrolledPastRecent = Boolean( viewMode === VIEW_MODES.CHAT && !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]); const commentsRef = React.createRef(); function toggleSuperChat() { if (superChatsChannelUrls && superChatsChannelUrls.length > 0) { doResolveUris(superChatsChannelUrls, true); if (superChatsByAmount.length > LARGE_SUPER_CHAT_LIST_THRESHOLD) { setResolvingSuperChats(true); } } setViewMode(VIEW_MODES.SUPERCHAT); if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT); } React.useEffect(() => { if (customViewMode && customViewMode !== viewMode) { setViewMode(customViewMode); } }, [customViewMode, viewMode]); React.useEffect(() => { if (claimId) { doCommentList(uri, '', 1, 75); doSuperChatList(uri); } }, [claimId, uri, doCommentList, doSuperChatList]); React.useEffect(() => { if (isMobile && viewMode === VIEW_MODES.CHAT && !didInitialScroll) { restoreScrollPos(); setDidInitialScroll(true); } }, [didInitialScroll, isMobile, restoreScrollPos, viewMode]); // Register scroll handler (TODO: Should throttle/debounce) React.useEffect(() => { function handleScroll() { if (discussionElement) { const scrollTop = discussionElement.scrollTop; if (scrollTop !== scrollPos) { setScrollPos(scrollTop); } } } if (discussionElement) { discussionElement.addEventListener('scroll', handleScroll); return () => discussionElement.removeEventListener('scroll', handleScroll); } }, [discussionElement, 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]); // Stop spinner for resolving superchats React.useEffect(() => { if (resolvingSuperChats) { // The real solution to the sluggishness is to fix the claim store/selectors // and to paginate the long superchat list. This serves as a band-aid, // showing a spinner while we batch-resolve. The duration is just a rough // estimate -- the lag will handle the remaining time. const timer = setTimeout(() => { setResolvingSuperChats(false); // Scroll to the top: if (discussionElement) { const divHeight = discussionElement.scrollHeight; discussionElement.scrollTop = divHeight * -1; } }, 1000); return () => clearTimeout(timer); } }, [discussionElement, resolvingSuperChats]); if (!claim) return null; const chatContentToggle = (toggleMode: string, label: any) => (