From 6a1216d2cd1c26c22baf86ae618a62aa66817239 Mon Sep 17 00:00:00 2001 From: Les Orchard Date: Mon, 5 Nov 2018 12:59:41 -0500 Subject: [PATCH] Temporarily hold timeline if mouse moved recently (fixes #8630) (#9200) - On recent mouse movement, hold timeline position so statuses remain in place for interactions in progress. - If the timeline had been scrolled to the top before mouse movement, restore scroll on mouse idle. --- .../mastodon/components/scrollable_list.js | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 5c888650c..e51c83c2b 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -9,6 +9,8 @@ import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; +const MOUSE_IDLE_DELAY = 300; + export default class ScrollableList extends PureComponent { static contextTypes = { @@ -37,6 +39,8 @@ export default class ScrollableList extends PureComponent { state = { fullscreen: null, + mouseMovedRecently: false, + scrollToTopOnMouseIdle: false, }; intersectionObserverWrapper = new IntersectionObserverWrapper(); @@ -60,6 +64,47 @@ export default class ScrollableList extends PureComponent { trailing: true, }); + mouseIdleTimer = null; + + clearMouseIdleTimer = () => { + if (this.mouseIdleTimer === null) { + return; + } + clearTimeout(this.mouseIdleTimer); + this.mouseIdleTimer = null; + }; + + handleMouseMove = throttle(() => { + // As long as the mouse keeps moving, clear and restart the idle timer. + this.clearMouseIdleTimer(); + this.mouseIdleTimer = + setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + + this.setState(({ + mouseMovedRecently, + scrollToTopOnMouseIdle, + }) => ({ + mouseMovedRecently: true, + // Only set scrollToTopOnMouseIdle if we just started moving and were + // scrolled to the top. Otherwise, just retain the previous state. + scrollToTopOnMouseIdle: + mouseMovedRecently + ? scrollToTopOnMouseIdle + : (this.node.scrollTop === 0), + })); + }, MOUSE_IDLE_DELAY / 2); + + handleMouseIdle = () => { + if (this.state.scrollToTopOnMouseIdle) { + this.node.scrollTop = 0; + this.props.onScrollToTop(); + } + this.setState({ + mouseMovedRecently: false, + scrollToTopOnMouseIdle: false, + }); + } + componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); @@ -73,7 +118,7 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && this.node.scrollTop > 0) { + if ((someItemInserted && this.node.scrollTop > 0) || this.state.mouseMovedRecently) { return this.node.scrollHeight - this.node.scrollTop; } else { return null; @@ -93,6 +138,7 @@ export default class ScrollableList extends PureComponent { } componentWillUnmount () { + this.clearMouseIdleTimer(); this.detachScrollListener(); this.detachIntersectionObserver(); detachFullscreenListener(this.onFullScreenChange); @@ -151,7 +197,7 @@ export default class ScrollableList extends PureComponent { if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = ( -
+
{prepend}