From 4e1876fe85ab3a70eba50466a62bff8a9d01c16c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 18:46:27 -0500 Subject: [PATCH] Refactor the scroll-to-top UX --- src/lib/hooks/useOnMainScroll.ts | 58 ++++++++++----- src/view/com/notifications/Feed.tsx | 1 + src/view/com/posts/Feed.tsx | 3 +- src/view/com/util/fab/FABInner.tsx | 2 +- .../util/load-latest/LoadLatestBtnMobile.tsx | 39 ++++------- src/view/screens/CustomFeed.tsx | 70 +++++++------------ src/view/screens/Home.tsx | 11 +-- src/view/screens/Notifications.tsx | 16 +++-- src/view/screens/SearchMobile.tsx | 2 +- 9 files changed, 102 insertions(+), 100 deletions(-) diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 994a3571..782c4704 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -1,28 +1,50 @@ -import {useState} from 'react' +import {useState, useCallback, useRef} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {RootStoreModel} from 'state/index' +import {s} from 'lib/styles' -export type onMomentumScrollEndCb = ( - event: NativeSyntheticEvent, -) => void export type OnScrollCb = ( event: NativeSyntheticEvent, ) => void +export type ResetCb = () => void -export function useOnMainScroll(store: RootStoreModel) { - let [lastY, setLastY] = useState(0) - let isMinimal = store.shell.minimalShellMode - return function onMainScroll(event: NativeSyntheticEvent) { - const y = event.nativeEvent.contentOffset.y - const dy = y - (lastY || 0) - setLastY(y) +export function useOnMainScroll( + store: RootStoreModel, +): [OnScrollCb, boolean, ResetCb] { + let lastY = useRef(0) + let [isScrolledDown, setIsScrolledDown] = useState(false) + return [ + useCallback( + (event: NativeSyntheticEvent) => { + const y = event.nativeEvent.contentOffset.y + const dy = y - (lastY.current || 0) + lastY.current = y - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { + if (!store.shell.minimalShellMode && y > 10 && dy > 10) { + store.shell.setMinimalShellMode(true) + } else if (store.shell.minimalShellMode && (y <= 10 || dy < -10)) { + store.shell.setMinimalShellMode(false) + } + + if ( + !isScrolledDown && + event.nativeEvent.contentOffset.y > s.window.height + ) { + setIsScrolledDown(true) + } else if ( + isScrolledDown && + event.nativeEvent.contentOffset.y < s.window.height + ) { + setIsScrolledDown(false) + } + }, + [store, isScrolledDown], + ), + isScrolledDown, + useCallback(() => { + setIsScrolledDown(false) store.shell.setMinimalShellMode(false) - isMinimal = false - } - } + lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf + }, [store, setIsScrolledDown]), + ] } diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 50bdc5dc..d457d713 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -154,6 +154,7 @@ export const Feed = observer(function Feed({ onEndReached={onEndReached} onEndReachedThreshold={0.6} onScroll={onScroll} + scrollEventThrottle={100} contentContainerStyle={s.contentContainer} /> ) : null} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 2726ff7d..b9021347 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -14,7 +14,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' @@ -47,7 +47,6 @@ export const Feed = observer(function Feed({ onPressTryAgain?: () => void onScroll?: OnScrollCb scrollEventThrottle?: number - onMomentumScrollEnd?: onMomentumScrollEndCb renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 5eb4a658..76824e57 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ outer: { position: 'absolute', zIndex: 1, - right: 28, + right: 24, bottom: 94, width: 60, height: 60, diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 548d30d5..5e03e228 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -1,23 +1,25 @@ import React from 'react' import {StyleSheet, TouchableOpacity} from 'react-native' import {observer} from 'mobx-react-lite' -import LinearGradient from 'react-native-linear-gradient' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {Text} from '../text/Text' -import {colors, gradients} from 'lib/styles' import {clamp} from 'lodash' import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} export const LoadLatestBtn = observer( ({onPress, label}: {onPress: () => void; label: string}) => { const store = useStores() + const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() return ( - - - {label} - - + accessibilityHint=""> + ) }, @@ -44,19 +38,14 @@ export const LoadLatestBtn = observer( const styles = StyleSheet.create({ loadLatest: { position: 'absolute', - left: 20, + left: 18, bottom: 35, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowOffset: {width: 0, height: 1}, - }, - loadLatestInner: { + borderWidth: 1, + width: 52, + height: 52, + borderRadius: 26, flexDirection: 'row', - paddingHorizontal: 14, - paddingVertical: 10, - borderRadius: 30, - }, - loadLatestText: { - color: colors.white, + alignItems: 'center', + justifyContent: 'center', }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index dcb72687..1409762d 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -20,13 +20,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb, isWeb} from 'platform/detection' +import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -48,7 +48,8 @@ export const CustomFeedScreen = withAuthRequired( return feed }, [store, uri]) const isPinned = store.me.savedFeeds.isPinned(uri) - const [allowScrollToTop, setAllowScrollToTop] = useState(false) + const [onMainScroll, isScrolledDown, resetMainScroll] = + useOnMainScroll(store) useSetTitle(currentFeed?.displayName) const onToggleSaved = React.useCallback(async () => { @@ -66,6 +67,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up update feeds', {err}) } }, [store, currentFeed]) + const onToggleLiked = React.useCallback(async () => { Haptics.default() try { @@ -81,6 +83,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up toggle like', {err}) } }, [store, currentFeed]) + const onTogglePinned = React.useCallback(async () => { Haptics.default() store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { @@ -88,11 +91,17 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed to toggle pinned feed', {e}) }) }, [store, currentFeed]) + const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/feed/${rkey}`) shareUrl(url) }, [name, rkey]) + const onScrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + resetMainScroll() + }, [scrollElRef, resetMainScroll]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -220,15 +229,17 @@ export const CustomFeedScreen = withAuthRequired( ) : null} - + {currentFeed ? ( + + ) : null}