Fix livestream autoscroll

## Ticket
6886 Livestream auto-scroll problems

## Issue
- `performedInitialScroll` was problematic as it won't allow auto-scroll even when user scrolled back to the bottom.
- The dependence on `commentElement` seems to assume a certain comment height? Not sure.

## Approach
- Add a scroll listener and stash the last scroll position.
- When a message is received, check if it's at the bottom. If yes, maintain that position after the new comment is added. If not, leave as is.
- When submitting a comment, always reset to the bottom.
This commit is contained in:
infinite-persistence 2021-08-16 22:14:29 +08:00
parent 05d358ac0b
commit e1fc5fd6e3
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0

View file

@ -28,7 +28,6 @@ type Props = {
const VIEW_MODE_CHAT = 'view_chat'; const VIEW_MODE_CHAT = 'view_chat';
const VIEW_MODE_SUPER_CHAT = 'view_superchat'; const VIEW_MODE_SUPER_CHAT = 'view_superchat';
const COMMENT_SCROLL_OFFSET = 100;
const COMMENT_SCROLL_TIMEOUT = 25; const COMMENT_SCROLL_TIMEOUT = 25;
export default function LivestreamComments(props: Props) { export default function LivestreamComments(props: Props) {
@ -50,15 +49,13 @@ export default function LivestreamComments(props: Props) {
let superChatsFiatAmount, superChatsTotalAmount; let superChatsFiatAmount, superChatsTotalAmount;
const commentsRef = React.createRef(); const commentsRef = React.createRef();
const [scrollBottom, setScrollBottom] = React.useState(true);
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT); const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false); const [scrollPos, setScrollPos] = React.useState(0);
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length; const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount; const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
const discussionElement = document.querySelector('.livestream__comments'); const discussionElement = document.querySelector('.livestream__comments');
const commentElement = document.querySelector('.livestream-comment');
let pinnedComment; let pinnedComment;
const pinnedCommentIds = (claimId && pinnedCommentsById[claimId]) || []; const pinnedCommentIds = (claimId && pinnedCommentsById[claimId]) || [];
@ -66,6 +63,12 @@ export default function LivestreamComments(props: Props) {
pinnedComment = commentsByChronologicalOrder.find((c) => c.comment_id === pinnedCommentIds[0]); pinnedComment = commentsByChronologicalOrder.find((c) => c.comment_id === pinnedCommentIds[0]);
} }
function restoreScrollPos() {
if (discussionElement) {
discussionElement.scrollTop = 0;
}
}
React.useEffect(() => { React.useEffect(() => {
if (claimId) { if (claimId) {
doCommentList(uri, '', 1, 75); doCommentList(uri, '', 1, 75);
@ -80,36 +83,39 @@ export default function LivestreamComments(props: Props) {
}; };
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]); }, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
const handleScroll = React.useCallback(() => { // Register scroll handler (TODO: Should throttle/debounce)
if (discussionElement) {
const negativeCommentHeight = commentElement && -1 * commentElement.offsetHeight;
const isAtRecent = negativeCommentHeight && discussionElement.scrollTop >= negativeCommentHeight;
setScrollBottom(isAtRecent);
}
}, [commentElement, discussionElement]);
React.useEffect(() => { React.useEffect(() => {
if (discussionElement) { function handleScroll() {
discussionElement.addEventListener('scroll', handleScroll); if (discussionElement) {
const scrollTop = discussionElement.scrollTop;
if (commentsLength > 0) { if (scrollTop !== scrollPos) {
// Only update comment scroll if the user hasn't scrolled up to view old comments setScrollPos(scrollTop);
// If they have, do nothing
if (!performedInitialScroll) {
setTimeout(
() =>
(discussionElement.scrollTop =
discussionElement.scrollHeight - discussionElement.offsetHeight + COMMENT_SCROLL_OFFSET),
COMMENT_SCROLL_TIMEOUT
);
setPerformedInitialScroll(true);
} }
} }
}
if (discussionElement) {
discussionElement.addEventListener('scroll', handleScroll);
return () => discussionElement.removeEventListener('scroll', handleScroll); return () => discussionElement.removeEventListener('scroll', handleScroll);
} }
}, [commentsLength, discussionElement, handleScroll, performedInitialScroll, setPerformedInitialScroll]); }, [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
if (scrollPos >= 0) {
// +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.
discussionElement.scrollTop = 0;
}, COMMENT_SCROLL_TIMEOUT);
return () => clearTimeout(timer);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [commentsLength]); // (Just respond to 'commentsLength' updates and nothing else)
// sum total amounts for fiat tips and lbc tips // sum total amounts for fiat tips and lbc tips
if (superChatsByTipAmount) { if (superChatsByTipAmount) {
@ -162,13 +168,6 @@ export default function LivestreamComments(props: Props) {
return null; return null;
} }
function scrollBack() {
if (discussionElement) {
discussionElement.scrollTop = 0;
setScrollBottom(true);
}
}
return ( return (
<div className="card livestream__discussion"> <div className="card livestream__discussion">
<div className="card__header--between livestream-discussion__header"> <div className="card__header--between livestream-discussion__header">
@ -184,8 +183,7 @@ export default function LivestreamComments(props: Props) {
onClick={() => { onClick={() => {
setViewMode(VIEW_MODE_CHAT); setViewMode(VIEW_MODE_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0]; const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
const divHeight = livestreamCommentsDiv.scrollHeight; livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
livestreamCommentsDiv.scrollTop = divHeight;
}} }}
/> />
@ -301,17 +299,17 @@ export default function LivestreamComments(props: Props) {
<div className="main--empty" style={{ flex: 1 }} /> <div className="main--empty" style={{ flex: 1 }} />
)} )}
{!scrollBottom && ( {scrollPos < 0 && (
<Button <Button
button="alt" button="alt"
className="livestream__comments-scroll__down" className="livestream__comments-scroll__down"
label={__('Recent Comments')} label={__('Recent Comments')}
onClick={scrollBack} onClick={restoreScrollPos}
/> />
)} )}
<div className="livestream__comment-create"> <div className="livestream__comment-create">
<CommentCreate livestream bottom embed={embed} uri={uri} /> <CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
</div> </div>
</div> </div>
</> </>