451 lines
15 KiB
JavaScript
451 lines
15 KiB
JavaScript
// @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<Comment>,
|
|
pinnedComments: Array<Comment>,
|
|
superChats: Array<Comment>,
|
|
theme: string,
|
|
doCommentList: (
|
|
uri: string,
|
|
parentId: ?string,
|
|
page: number,
|
|
pageSize: number,
|
|
sortBy: ?number,
|
|
isLivestream: boolean
|
|
) => void,
|
|
doResolveUris: (uris: Array<string>, 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 (
|
|
<div className="card livestream__chat">
|
|
<div className="card__header--between livestreamDiscussion__header">
|
|
<div className="card__title-section--small livestreamDiscussion__title">{__('Live Chat')}</div>
|
|
</div>
|
|
|
|
<div className="livestreamComments__wrapper">
|
|
<div className="main--empty">
|
|
<Yrbl
|
|
title={__('Chat Hidden')}
|
|
actions={
|
|
<div className="section__actions">
|
|
{openedPopoutWindow && (
|
|
<Button
|
|
button="secondary"
|
|
label={__('Close Popout')}
|
|
onClick={() => {
|
|
openedPopoutWindow.close();
|
|
setPopoutWindow(undefined);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{chatHidden && (
|
|
<Button button="secondary" label={__('Show Chat')} onClick={() => setChatHidden(false)} />
|
|
)}
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="livestream__comment-create">
|
|
<CommentCreate isLivestream bottom uri={uri} disableInput />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const toggleProps = { viewMode, onClick: (toggleMode) => toggleClick(toggleMode) };
|
|
|
|
return (
|
|
<div className={classnames('card livestream__chat', { 'livestream__chat--popout': isPopoutWindow })}>
|
|
{!hideHeader && (
|
|
<div className="card__header--between livestreamDiscussion__header">
|
|
<div className="card__title-section--small livestreamDiscussion__title">
|
|
{__('Live Chat')}
|
|
|
|
<LivestreamMenu
|
|
isPopoutWindow={isPopoutWindow}
|
|
hideChat={() => setChatHidden(true)}
|
|
setPopoutWindow={(v) => setPopoutWindow(v)}
|
|
isMobile={isMobile}
|
|
/>
|
|
</div>
|
|
|
|
{superChatsByChronologicalOrder && (
|
|
<div className="recommended-content__toggles">
|
|
{/* the superchats in chronological order button */}
|
|
<ChatContentToggle {...toggleProps} toggleMode={VIEW_MODES.CHAT} label={__('Chat')} />
|
|
|
|
{/* the button to show superchats listed by most to least support amount */}
|
|
<ChatContentToggle
|
|
{...toggleProps}
|
|
toggleMode={VIEW_MODES.SUPERCHAT}
|
|
label={
|
|
<>
|
|
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
|
|
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="livestreamComments__wrapper">
|
|
<div
|
|
className={classnames('livestream-comments__top-actions', {
|
|
'livestream-comments__top-actions--mobile': isMobile,
|
|
})}
|
|
>
|
|
{isMobile && ((pinnedComment && showPinned) || (superChatsByAmount && !superchatsHidden)) && (
|
|
<MobileDrawerTopGradient theme={theme} />
|
|
)}
|
|
|
|
{viewMode === VIEW_MODES.CHAT && superChatsByAmount && (
|
|
<LivestreamSuperchats
|
|
superChats={superChatsByAmount}
|
|
toggleSuperChat={toggleSuperChat}
|
|
superchatsHidden={superchatsHidden}
|
|
isMobile={isMobile}
|
|
/>
|
|
)}
|
|
|
|
{pinnedComment &&
|
|
viewMode === VIEW_MODES.CHAT &&
|
|
(isMobile ? (
|
|
<Slide direction="left" in={showPinned} mountOnEnter unmountOnExit>
|
|
<div className="livestream-pinned__wrapper--mobile">
|
|
<LivestreamComment
|
|
comment={pinnedComment}
|
|
key={pinnedComment.comment_id}
|
|
uri={uri}
|
|
handleDismissPin={() => setShowPinned(false)}
|
|
isMobile
|
|
setResolvingSuperChats={setResolvingSuperChats}
|
|
/>
|
|
</div>
|
|
</Slide>
|
|
) : (
|
|
showPinned && (
|
|
<div className="livestream-pinned__wrapper">
|
|
<LivestreamComment comment={pinnedComment} key={pinnedComment.comment_id} uri={uri} />
|
|
|
|
<Button
|
|
title={__('Dismiss pinned comment')}
|
|
button="inverse"
|
|
className="close-button"
|
|
onClick={() => setShowPinned(false)}
|
|
icon={ICONS.REMOVE}
|
|
/>
|
|
</div>
|
|
)
|
|
))}
|
|
</div>
|
|
|
|
<LivestreamComments
|
|
uri={uri}
|
|
viewMode={viewMode}
|
|
comments={commentsToDisplay}
|
|
isMobile={isMobile}
|
|
restoreScrollPos={!scrolledPastRecent && isMobile && restoreScrollPos}
|
|
/>
|
|
|
|
{scrolledPastRecent && (
|
|
<Button
|
|
button="secondary"
|
|
className="livestream-comments__scroll-to-recent"
|
|
label={viewMode === VIEW_MODES.CHAT ? __('Recent Comments') : __('Recent Tips')}
|
|
onClick={restoreScrollPos}
|
|
iconRight={ICONS.DOWN}
|
|
/>
|
|
)}
|
|
|
|
<div className="livestream__comment-create">
|
|
<CommentCreate
|
|
isLivestream
|
|
bottom
|
|
embed={embed}
|
|
uri={uri}
|
|
onDoneReplying={restoreScrollPos}
|
|
onSlimInputClose={!scrolledPastRecent && isMobile ? () => setKeyboardOpened(true) : undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type ToggleProps = {
|
|
viewMode: string,
|
|
toggleMode: string,
|
|
label: string | any,
|
|
onClick: (string) => void,
|
|
};
|
|
|
|
const ChatContentToggle = (props: ToggleProps) => {
|
|
const { viewMode, toggleMode, label, onClick } = props;
|
|
|
|
return (
|
|
<Button
|
|
className={classnames('button-toggle', { 'button-toggle--active': viewMode === toggleMode })}
|
|
label={label}
|
|
onClick={() => onClick(toggleMode)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
type GradientProps = {
|
|
theme: string,
|
|
};
|
|
|
|
const MobileDrawerTopGradient = (gradientProps: GradientProps) => {
|
|
const { theme } = gradientProps;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
background: `linear-gradient(180deg, ${theme === 'light' ? grey[300] : grey[900]} 0, transparent 65%)`,
|
|
}}
|
|
className="livestream__top-gradient"
|
|
/>
|
|
);
|
|
};
|