about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js50
1 files changed, 48 insertions, 2 deletions
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index 21d717b81..006c2a899 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -10,6 +10,8 @@ import classNames from 'classnames';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 import LoadingIndicator from './loading_indicator';
 
+const MOUSE_IDLE_DELAY = 300;
+
 export default class ScrollableList extends PureComponent {
 
   static contextTypes = {
@@ -38,6 +40,8 @@ export default class ScrollableList extends PureComponent {
 
   state = {
     fullscreen: null,
+    mouseMovedRecently: false,
+    scrollToTopOnMouseIdle: false,
   };
 
   intersectionObserverWrapper = new IntersectionObserverWrapper();
@@ -61,6 +65,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();
@@ -90,7 +135,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;
@@ -104,6 +149,7 @@ export default class ScrollableList extends PureComponent {
   }
 
   componentWillUnmount () {
+    this.clearMouseIdleTimer();
     this.detachScrollListener();
     this.detachIntersectionObserver();
     detachFullscreenListener(this.onFullScreenChange);
@@ -181,7 +227,7 @@ export default class ScrollableList extends PureComponent {
       );
     } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
       scrollableArea = (
-        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
+        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
           <div role='feed' className='item-list'>
             {prepend}