Refactor the scroll-to-top UX
This commit is contained in:
parent
9673225f78
commit
4e1876fe85
9 changed files with 102 additions and 100 deletions
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}),
|
||||
|
|
|
@ -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>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue