Refactor the scroll-to-top UX

This commit is contained in:
Paul Frazee 2023-05-24 18:46:27 -05:00
parent 9673225f78
commit 4e1876fe85
9 changed files with 102 additions and 100 deletions

View file

@ -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<CommonNavigatorParams, 'CustomFeed'>
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 (
<View style={styles.headerBtns}>
@ -220,15 +229,17 @@ export const CustomFeedScreen = withAuthRequired(
</Text>
) : null}
<View style={styles.headerDetailsFooter}>
<TextLink
type="md-medium"
style={pal.textLight}
href={`/profile/${name}/feed/${rkey}/liked-by`}
text={`Liked by ${currentFeed?.data.likeCount} ${pluralize(
currentFeed?.data.likeCount || 0,
'user',
)}`}
/>
{currentFeed ? (
<TextLink
type="md-medium"
style={pal.textLight}
href={`/profile/${name}/feed/${rkey}/liked-by`}
text={`Liked by ${currentFeed.data.likeCount} ${pluralize(
currentFeed?.data.likeCount || 0,
'user',
)}`}
/>
) : null}
<Button
type={'default'}
accessibilityLabel={
@ -267,46 +278,19 @@ export const CustomFeedScreen = withAuthRequired(
onTogglePinned,
])
const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback(
event => {
console.log('onMomentumScrollEnd')
if (event.nativeEvent.contentOffset.y > s.window.height * 3) {
setAllowScrollToTop(true)
} else {
setAllowScrollToTop(false)
}
},
[],
)
const onScroll: OnScrollCb = React.useCallback(event => {
// since onMomentumScrollEnd is not supported in react-native-web, we have to use onScroll which fires more often so is not desirable on mobile
if (isWeb) {
if (event.nativeEvent.contentOffset.y > s.window.height * 2) {
setAllowScrollToTop(true)
} else {
setAllowScrollToTop(false)
}
}
}, [])
return (
<View style={s.hContentRegion}>
<ViewHeader title="" renderButton={renderHeaderBtns} />
<Feed
scrollElRef={scrollElRef}
feed={algoFeed}
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={onScroll} // same logic as onMomentumScrollEnd but for web
onScroll={onMainScroll}
scrollEventThrottle={100}
ListHeaderComponent={renderListHeaderComponent}
extraData={[uri, isPinned]}
/>
{allowScrollToTop ? (
<LoadLatestBtn
onPress={() => {
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
}}
label="Scroll to top"
/>
{isScrolledDown ? (
<LoadLatestBtn onPress={onScrollToTop} label="Scroll to top" />
) : null}
</View>
)

View file

@ -150,7 +150,8 @@ const FeedPage = observer(
renderEmptyState?: () => JSX.Element
}) => {
const store = useStores()
const onMainScroll = useOnMainScroll(store)
const [onMainScroll, isScrolledDown, resetMainScroll] =
useOnMainScroll(store)
const {screen, track} = useAnalytics()
const scrollElRef = React.useRef<FlatList>(null)
const {appState} = useAppState({
@ -178,12 +179,13 @@ const FeedPage = observer(
const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -HEADER_OFFSET})
}, [scrollElRef])
resetMainScroll()
}, [scrollElRef, resetMainScroll])
const onSoftReset = React.useCallback(() => {
if (isPageFocused) {
feed.refresh()
scrollToTop()
feed.refresh()
}
}, [isPageFocused, scrollToTop, feed])
@ -254,10 +256,11 @@ const FeedPage = observer(
showPostFollowBtn
onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll}
scrollEventThrottle={100}
renderEmptyState={renderEmptyState}
headerOffset={HEADER_OFFSET}
/>
{feed.hasNewLatest && !feed.isRefreshing && (
{isScrolledDown && (
<LoadLatestBtn onPress={onPressLoadLatest} label="Load new posts" />
)}
<FAB

View file

@ -25,7 +25,8 @@ type Props = NativeStackScreenProps<
export const NotificationsScreen = withAuthRequired(
observer(({}: Props) => {
const store = useStores()
const onMainScroll = useOnMainScroll(store)
const [onMainScroll, isScrolledDown, resetMainScroll] =
useOnMainScroll(store)
const scrollElRef = React.useRef<FlatList>(null)
const {screen} = useAnalytics()
@ -37,7 +38,8 @@ export const NotificationsScreen = withAuthRequired(
const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: 0})
}, [scrollElRef])
resetMainScroll()
}, [scrollElRef, resetMainScroll])
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
@ -96,10 +98,12 @@ export const NotificationsScreen = withAuthRequired(
onScroll={onMainScroll}
scrollElRef={scrollElRef}
/>
{store.me.notifications.hasNewLatest &&
!store.me.notifications.isRefreshing && (
<LoadLatestBtn onPress={onPressLoadLatest} label="Load new notifications" />
)}
{isScrolledDown && (
<LoadLatestBtn
onPress={onPressLoadLatest}
label="Load new notifications"
/>
)}
</View>
)
}),

View file

@ -35,7 +35,7 @@ export const SearchScreen = withAuthRequired(
const store = useStores()
const scrollViewRef = React.useRef<ScrollView>(null)
const flatListRef = React.useRef<FlatList>(null)
const onMainScroll = useOnMainScroll(store)
const [onMainScroll] = useOnMainScroll(store)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('')
const autocompleteView = React.useMemo<UserAutocompleteModel>(