diff --git a/src/components/scroll-top-button.tsx b/src/components/scroll-top-button.tsx index 2a1ddd812..84f23969e 100644 --- a/src/components/scroll-top-button.tsx +++ b/src/components/scroll-top-button.tsx @@ -1,10 +1,8 @@ -import clsx from 'clsx'; import throttle from 'lodash/throttle'; import React, { useState, useEffect, useCallback } from 'react'; import { useIntl, MessageDescriptor } from 'react-intl'; -import Icon from 'soapbox/components/icon'; -import { Text } from 'soapbox/components/ui'; +import { Icon, Text } from 'soapbox/components/ui'; import { useSettings } from 'soapbox/hooks'; interface IScrollTopButton { @@ -31,67 +29,77 @@ const ScrollTopButton: React.FC = ({ const intl = useIntl(); const settings = useSettings(); + // Whether we are scrolled past the `threshold`. const [scrolled, setScrolled] = useState(false); - const autoload = settings.get('autoloadTimelines') === true; + // Whether we are scrolled above the `autoloadThreshold`. + const [scrolledTop, setScrolledTop] = useState(false); + const autoload = settings.get('autoloadTimelines') === true; const visible = count > 0 && scrolled; - const classes = clsx('fixed left-1/2 top-20 z-50 -translate-x-1/2', { - 'hidden': !visible, - }); - + /** Number of pixels scrolled down from the top of the page. */ const getScrollTop = (): number => { return (document.scrollingElement || document.documentElement).scrollTop; }; - const maybeUnload = () => { - if (autoload && getScrollTop() <= autoloadThreshold) { + /** Unload feed items if scrolled to the top. */ + const maybeUnload = useCallback(() => { + if (autoload && scrolledTop) { onClick(); } - }; + }, [autoload, scrolledTop, onClick]); + /** Set state while scrolling. */ const handleScroll = useCallback(throttle(() => { - maybeUnload(); + const scrollTop = getScrollTop(); - if (getScrollTop() > threshold) { - setScrolled(true); - } else { - setScrolled(false); - } - }, 150, { trailing: true }), [autoload, threshold, autoloadThreshold, onClick]); + setScrolled(scrollTop > threshold); + setScrolledTop(scrollTop <= autoloadThreshold); - const scrollUp = () => { + }, 150, { trailing: true }), [threshold, autoloadThreshold]); + + /** Scroll to top and trigger `onClick`. */ + const handleClick: React.MouseEventHandler = useCallback(() => { window.scrollTo({ top: 0 }); - }; - - const handleClick: React.MouseEventHandler = () => { - setTimeout(scrollUp, 10); onClick(); - }; + }, [onClick]); useEffect(() => { - window.addEventListener('scroll', handleScroll); + // Delay adding the scroll listener so navigating back doesn't + // unload feed items before the feed is rendered. + setTimeout(() => { + window.addEventListener('scroll', handleScroll); + handleScroll(); + }, 250); return () => { window.removeEventListener('scroll', handleScroll); }; - }, [onClick]); + }, [handleScroll]); useEffect(() => { maybeUnload(); - }, [count]); + }, [maybeUnload]); + + if (!visible) { + return null; + } return ( -
- - +
+
); }; diff --git a/src/features/notifications/index.tsx b/src/features/notifications/index.tsx index 794073330..0d9f7f608 100644 --- a/src/features/notifications/index.tsx +++ b/src/features/notifications/index.tsx @@ -14,7 +14,7 @@ import { getSettings } from 'soapbox/actions/settings'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import ScrollTopButton from 'soapbox/components/scroll-top-button'; import ScrollableList from 'soapbox/components/scrollable-list'; -import { Column } from 'soapbox/components/ui'; +import { Column, Portal } from 'soapbox/components/ui'; import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification'; import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; @@ -104,13 +104,13 @@ const Notifications = () => { }); }; - const handleDequeueNotifications = () => { + const handleDequeueNotifications = useCallback(() => { dispatch(dequeueNotifications()); - }; + }, []); - const handleRefresh = () => { + const handleRefresh = useCallback(() => { return dispatch(expandNotifications()); - }; + }, []); useEffect(() => { handleDequeueNotifications(); @@ -176,11 +176,15 @@ const Notifications = () => { return ( {filterBarContainer} - + + + + + {scrollContainer} diff --git a/src/features/ui/components/timeline.tsx b/src/features/ui/components/timeline.tsx index a4d7d44e6..4db80bf90 100644 --- a/src/features/ui/components/timeline.tsx +++ b/src/features/ui/components/timeline.tsx @@ -6,6 +6,7 @@ import { defineMessages } from 'react-intl'; import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines'; import ScrollTopButton from 'soapbox/components/scroll-top-button'; import StatusList, { IStatusList } from 'soapbox/components/status-list'; +import { Portal } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetStatusIds } from 'soapbox/selectors'; @@ -37,9 +38,9 @@ const Timeline: React.FC = ({ const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true); const totalQueuedItemsCount = useAppSelector(state => state.timelines.get(timelineId)?.totalQueuedItemsCount || 0); - const handleDequeueTimeline = () => { + const handleDequeueTimeline = useCallback(() => { dispatch(dequeueTimeline(timelineId, onLoadMore)); - }; + }, []); const handleScrollToTop = useCallback(debounce(() => { dispatch(scrollTopTimeline(timelineId, true)); @@ -51,12 +52,14 @@ const Timeline: React.FC = ({ return ( <> - + + +