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_SUPER_CHAT = 'view_superchat';
const COMMENT_SCROLL_OFFSET = 100;
const COMMENT_SCROLL_TIMEOUT = 25;
export default function LivestreamComments(props: Props) {
@ -50,15 +49,13 @@ export default function LivestreamComments(props: Props) {
let superChatsFiatAmount, superChatsTotalAmount;
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 [scrollPos, setScrollPos] = React.useState(0);
const claimId = claim && claim.claim_id;
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
const discussionElement = document.querySelector('.livestream__comments');
const commentElement = document.querySelector('.livestream-comment');
let pinnedComment;
const pinnedCommentIds = (claimId && pinnedCommentsById[claimId]) || [];
@ -66,6 +63,12 @@ export default function LivestreamComments(props: Props) {
pinnedComment = commentsByChronologicalOrder.find((c) => c.comment_id === pinnedCommentIds[0]);
}
function restoreScrollPos() {
if (discussionElement) {
discussionElement.scrollTop = 0;
}
}
React.useEffect(() => {
if (claimId) {
doCommentList(uri, '', 1, 75);
@ -80,36 +83,39 @@ export default function LivestreamComments(props: Props) {
};
}, [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]);
// Register scroll handler (TODO: Should throttle/debounce)
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);
function handleScroll() {
if (discussionElement) {
const scrollTop = discussionElement.scrollTop;
if (scrollTop !== scrollPos) {
setScrollPos(scrollTop);
}
}
}
if (discussionElement) {
discussionElement.addEventListener('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
if (superChatsByTipAmount) {
@ -162,13 +168,6 @@ export default function LivestreamComments(props: Props) {
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">
@ -184,8 +183,7 @@ export default function LivestreamComments(props: Props) {
onClick={() => {
setViewMode(VIEW_MODE_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
const divHeight = livestreamCommentsDiv.scrollHeight;
livestreamCommentsDiv.scrollTop = divHeight;
livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
}}
/>
@ -301,17 +299,17 @@ export default function LivestreamComments(props: Props) {
<div className="main--empty" style={{ flex: 1 }} />
)}
{!scrollBottom && (
{scrollPos < 0 && (
<Button
button="alt"
className="livestream__comments-scroll__down"
label={__('Recent Comments')}
onClick={scrollBack}
onClick={restoreScrollPos}
/>
)}
<div className="livestream__comment-create">
<CommentCreate livestream bottom embed={embed} uri={uri} />
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
</div>
</div>
</>