about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-05-24 18:46:27 -0500
committerPaul Frazee <pfrazee@gmail.com>2023-05-24 18:46:27 -0500
commit4e1876fe85ab3a70eba50466a62bff8a9d01c16c (patch)
tree1d58eb7716587566c4eb1bae15ccbaf32240c075
parent9673225f78f656038b2db11062d8e397c81568bf (diff)
downloadvoidsky-4e1876fe85ab3a70eba50466a62bff8a9d01c16c.tar.zst
Refactor the scroll-to-top UX
-rw-r--r--src/lib/hooks/useOnMainScroll.ts58
-rw-r--r--src/view/com/notifications/Feed.tsx1
-rw-r--r--src/view/com/posts/Feed.tsx3
-rw-r--r--src/view/com/util/fab/FABInner.tsx2
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtnMobile.tsx39
-rw-r--r--src/view/screens/CustomFeed.tsx70
-rw-r--r--src/view/screens/Home.tsx11
-rw-r--r--src/view/screens/Notifications.tsx16
-rw-r--r--src/view/screens/SearchMobile.tsx2
9 files changed, 102 insertions, 100 deletions
diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts
index 994a35714..782c4704b 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<NativeScrollEvent>,
-) => void
 export type OnScrollCb = (
   event: NativeSyntheticEvent<NativeScrollEvent>,
 ) => void
+export type ResetCb = () => void
+
+export function useOnMainScroll(
+  store: RootStoreModel,
+): [OnScrollCb, boolean, ResetCb] {
+  let lastY = useRef(0)
+  let [isScrolledDown, setIsScrolledDown] = useState(false)
+  return [
+    useCallback(
+      (event: NativeSyntheticEvent<NativeScrollEvent>) => {
+        const y = event.nativeEvent.contentOffset.y
+        const dy = y - (lastY.current || 0)
+        lastY.current = y
 
-export function useOnMainScroll(store: RootStoreModel) {
-  let [lastY, setLastY] = useState(0)
-  let isMinimal = store.shell.minimalShellMode
-  return function onMainScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {
-    const y = event.nativeEvent.contentOffset.y
-    const dy = y - (lastY || 0)
-    setLastY(y)
+        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 (!isMinimal && y > 10 && dy > 10) {
-      store.shell.setMinimalShellMode(true)
-      isMinimal = true
-    } else if (isMinimal && (y <= 10 || dy < -10)) {
+        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 50bdc5dc9..d457d7136 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 2726ff7d3..b90213472 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 5eb4a6588..76824e575 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 548d30d5a..5e03e2285 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 (
       <TouchableOpacity
         style={[
           styles.loadLatest,
+          pal.borderDark,
+          pal.view,
           !store.shell.minimalShellMode && {
             bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
           },
@@ -26,16 +28,8 @@ export const LoadLatestBtn = observer(
         hitSlop={HITSLOP}
         accessibilityRole="button"
         accessibilityLabel={label}
-        accessibilityHint={label}>
-        <LinearGradient
-          colors={[gradients.blueLight.start, gradients.blueLight.end]}
-          start={{x: 0, y: 0}}
-          end={{x: 1, y: 1}}
-          style={styles.loadLatestInner}>
-          <Text type="md-bold" style={styles.loadLatestText}>
-            {label}
-          </Text>
-        </LinearGradient>
+        accessibilityHint="">
+        <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
       </TouchableOpacity>
     )
   },
@@ -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 dcb726873..1409762d1 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<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>
     )
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 4fe175fc1..bd800590d 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -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
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index df84b541b..02a4618c3 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -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>
     )
   }),
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
index f9b4864b2..c9d09373e 100644
--- a/src/view/screens/SearchMobile.tsx
+++ b/src/view/screens/SearchMobile.tsx
@@ -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>(