about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-12-12 16:04:14 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-12-12 16:04:14 -0600
commit1aec0ee156daa5a1d3e4ead70caf667edb75eebb (patch)
treee4931885b9dfea5996cad11e47e59f64109d4761 /src
parent470f444eed0d9643612bbdb9533cd64614834c69 (diff)
downloadvoidsky-1aec0ee156daa5a1d3e4ead70caf667edb75eebb.tar.zst
Hide footer on scroll down (minimal shell mode)
Diffstat (limited to 'src')
-rw-r--r--src/state/models/shell-ui.ts5
-rw-r--r--src/view/com/notifications/Feed.tsx4
-rw-r--r--src/view/com/posts/Feed.tsx4
-rw-r--r--src/view/com/util/ViewSelector.tsx11
-rw-r--r--src/view/lib/useOnMainScroll.ts25
-rw-r--r--src/view/screens/Home.tsx3
-rw-r--r--src/view/screens/Notifications.tsx8
-rw-r--r--src/view/screens/PostDownvotedBy.tsx1
-rw-r--r--src/view/screens/PostRepostedBy.tsx1
-rw-r--r--src/view/screens/PostThread.tsx1
-rw-r--r--src/view/screens/Profile.tsx3
-rw-r--r--src/view/screens/ProfileFollowers.tsx1
-rw-r--r--src/view/screens/ProfileFollows.tsx1
-rw-r--r--src/view/screens/ProfileMembers.tsx1
-rw-r--r--src/view/screens/Search.tsx1
-rw-r--r--src/view/screens/Settings.tsx1
-rw-r--r--src/view/shell/mobile/index.tsx37
17 files changed, 101 insertions, 7 deletions
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index 3199e421e..01e1cacad 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -74,6 +74,7 @@ export interface ComposerOpts {
 }
 
 export class ShellUiModel {
+  minimalShellMode = false
   isMainMenuOpen = false
   isModalActive = false
   activeModal:
@@ -91,6 +92,10 @@ export class ShellUiModel {
     makeAutoObservable(this)
   }
 
+  setMinimalShellMode(v: boolean) {
+    this.minimalShellMode = v
+  }
+
   setMainMenuOpen(v: boolean) {
     this.isMainMenuOpen = v
   }
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index a6af0f88a..c986bca57 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -6,15 +6,18 @@ import {FeedItem} from './FeedItem'
 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/ErrorMessage'
 import {EmptyState} from '../util/EmptyState'
+import {OnScrollCb} from '../../lib/useOnMainScroll'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 
 export const Feed = observer(function Feed({
   view,
   onPressTryAgain,
+  onScroll,
 }: {
   view: NotificationsViewModel
   onPressTryAgain?: () => void
+  onScroll?: OnScrollCb
 }) {
   // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
   //   VirtualizedList: You have a large list that is slow to update - make sure your
@@ -65,6 +68,7 @@ export const Feed = observer(function Feed({
           refreshing={view.isRefreshing}
           onRefresh={onRefresh}
           onEndReached={onEndReached}
+          onScroll={onScroll}
         />
       )}
     </View>
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 59b529dc4..e34513794 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -13,6 +13,7 @@ import {ErrorMessage} from '../util/ErrorMessage'
 import {FeedModel} from '../../../state/models/feed-view'
 import {FeedItem} from './FeedItem'
 import {ComposePrompt} from '../composer/Prompt'
+import {OnScrollCb} from '../../lib/useOnMainScroll'
 
 const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -23,12 +24,14 @@ export const Feed = observer(function Feed({
   scrollElRef,
   onPressCompose,
   onPressTryAgain,
+  onScroll,
 }: {
   feed: FeedModel
   style?: StyleProp<ViewStyle>
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressCompose: () => void
   onPressTryAgain?: () => void
+  onScroll?: OnScrollCb
 }) {
   // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
   //   VirtualizedList: You have a large list that is slow to update - make sure your
@@ -92,6 +95,7 @@ export const Feed = observer(function Feed({
           ListFooterComponent={FeedFooter}
           refreshing={feed.isRefreshing}
           contentContainerStyle={{paddingBottom: 100}}
+          onScroll={onScroll}
           onRefresh={onRefresh}
           onEndReached={onEndReached}
         />
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index 264a9086d..e436e41b2 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -1,8 +1,14 @@
 import React, {useEffect, useState} from 'react'
-import {FlatList, View} from 'react-native'
+import {
+  FlatList,
+  NativeSyntheticEvent,
+  NativeScrollEvent,
+  View,
+} from 'react-native'
 import {Selector} from './Selector'
 import {HorzSwipe} from './gestures/HorzSwipe'
 import {useAnimatedValue} from '../../lib/useAnimatedValue'
+import {OnScrollCb} from '../../lib/useOnMainScroll'
 
 const HEADER_ITEM = {_reactKey: '__header__'}
 const SELECTOR_ITEM = {_reactKey: '__selector__'}
@@ -17,6 +23,7 @@ export function ViewSelector({
   renderItem,
   ListFooterComponent,
   onSelectView,
+  onScroll,
   onRefresh,
   onEndReached,
 }: {
@@ -32,6 +39,7 @@ export function ViewSelector({
     | null
     | undefined
   onSelectView?: (viewIndex: number) => void
+  onScroll?: OnScrollCb
   onRefresh?: () => void
   onEndReached?: (info: {distanceFromEnd: number}) => void
 }) {
@@ -90,6 +98,7 @@ export function ViewSelector({
         ListFooterComponent={ListFooterComponent}
         stickyHeaderIndices={STICKY_HEADER_INDICES}
         refreshing={refreshing}
+        onScroll={onScroll}
         onRefresh={onRefresh}
         onEndReached={onEndReached}
       />
diff --git a/src/view/lib/useOnMainScroll.ts b/src/view/lib/useOnMainScroll.ts
new file mode 100644
index 000000000..ee0081226
--- /dev/null
+++ b/src/view/lib/useOnMainScroll.ts
@@ -0,0 +1,25 @@
+import {useState} from 'react'
+import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
+import {RootStoreModel} from '../../state'
+
+export type OnScrollCb = (
+  event: NativeSyntheticEvent<NativeScrollEvent>,
+) => void
+
+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 (!isMinimal && y > 10 && dy > 10) {
+      store.shell.setMinimalShellMode(true)
+      isMinimal = true
+    } else if (isMinimal && (y <= 10 || dy < -10)) {
+      store.shell.setMinimalShellMode(false)
+      isMinimal = false
+    }
+  }
+}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index a1d202738..078fb9800 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -9,6 +9,7 @@ import {useStores} from '../../state'
 import {FeedModel} from '../../state/models/feed-view'
 import {ScreenParams} from '../routes'
 import {s, colors} from '../lib/styles'
+import {useOnMainScroll} from '../lib/useOnMainScroll'
 
 const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
 
@@ -18,6 +19,7 @@ export const Home = observer(function Home({
   scrollElRef,
 }: ScreenParams) {
   const store = useStores()
+  const onMainScroll = useOnMainScroll(store)
   const [hasSetup, setHasSetup] = useState<boolean>(false)
   const {appState} = useAppState({
     onForeground: () => doPoll(true),
@@ -95,6 +97,7 @@ export const Home = observer(function Home({
         style={{flex: 1}}
         onPressCompose={onPressCompose}
         onPressTryAgain={onPressTryAgain}
+        onScroll={onMainScroll}
       />
       {defaultFeedView.hasNewLatest ? (
         <TouchableOpacity
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index b168ffaff..8af9d0713 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -5,9 +5,11 @@ import {Feed} from '../com/notifications/Feed'
 import {useStores} from '../../state'
 import {NotificationsViewModel} from '../../state/models/notifications-view'
 import {ScreenParams} from '../routes'
+import {useOnMainScroll} from '../lib/useOnMainScroll'
 
 export const Notifications = ({navIdx, visible}: ScreenParams) => {
   const store = useStores()
+  const onMainScroll = useOnMainScroll(store)
 
   useEffect(() => {
     if (!visible) {
@@ -33,7 +35,11 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
   return (
     <View style={{flex: 1}}>
       <ViewHeader title="Notifications" />
-      <Feed view={store.me.notifications} onPressTryAgain={onPressTryAgain} />
+      <Feed
+        view={store.me.notifications}
+        onPressTryAgain={onPressTryAgain}
+        onScroll={onMainScroll}
+      />
     </View>
   )
 }
diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx
index b16ec5c0a..ab110f8f9 100644
--- a/src/view/screens/PostDownvotedBy.tsx
+++ b/src/view/screens/PostDownvotedBy.tsx
@@ -14,6 +14,7 @@ export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
   useEffect(() => {
     if (visible) {
       store.nav.setTitle(navIdx, 'Downvoted by')
+      store.shell.setMinimalShellMode(false)
     }
   }, [store, visible])
 
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index d8e4b910e..4e84617df 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -14,6 +14,7 @@ export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
   useEffect(() => {
     if (visible) {
       store.nav.setTitle(navIdx, 'Reposted by')
+      store.shell.setMinimalShellMode(false)
     }
   }, [store, visible])
 
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 1e63ac390..4caf144bf 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -29,6 +29,7 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
       return
     }
     setTitle()
+    store.shell.setMinimalShellMode(false)
     if (!view.hasLoaded && !view.isLoading) {
       console.log('Fetching post thread', uri)
       view.setup().then(
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 93a7147b5..86be47fbd 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -18,6 +18,7 @@ import {EmptyState} from '../com/util/EmptyState'
 import {ViewHeader} from '../com/util/ViewHeader'
 import * as Toast from '../com/util/Toast'
 import {s, colors} from '../lib/styles'
+import {useOnMainScroll} from '../lib/useOnMainScroll'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const END_ITEM = {_reactKey: '__end__'}
@@ -25,6 +26,7 @@ const EMPTY_ITEM = {_reactKey: '__empty__'}
 
 export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
+  const onMainScroll = useOnMainScroll(store)
   const [hasSetup, setHasSetup] = useState<boolean>(false)
   const uiState = useMemo(
     () => new ProfileUiModel(store, {user: params.name}),
@@ -252,6 +254,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
           ListFooterComponent={Footer}
           refreshing={uiState.isRefreshing || false}
           onSelectView={onSelectView}
+          onScroll={onMainScroll}
           onRefresh={onRefresh}
           onEndReached={onEndReached}
         />
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index b19a5bc37..49b3e2e05 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -12,6 +12,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
   useEffect(() => {
     if (visible) {
       store.nav.setTitle(navIdx, `Followers of ${name}`)
+      store.shell.setMinimalShellMode(false)
     }
   }, [store, visible, name])
 
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index e54b562e7..58df6e76d 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -12,6 +12,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
   useEffect(() => {
     if (visible) {
       store.nav.setTitle(navIdx, `Followed by ${name}`)
+      store.shell.setMinimalShellMode(false)
     }
   }, [store, visible, name])
 
diff --git a/src/view/screens/ProfileMembers.tsx b/src/view/screens/ProfileMembers.tsx
index b4b6c7e50..9d6723fe9 100644
--- a/src/view/screens/ProfileMembers.tsx
+++ b/src/view/screens/ProfileMembers.tsx
@@ -12,6 +12,7 @@ export const ProfileMembers = ({navIdx, visible, params}: ScreenParams) => {
   useEffect(() => {
     if (visible) {
       store.nav.setTitle(navIdx, `Members of ${name}`)
+      store.shell.setMinimalShellMode(false)
     }
   }, [store, visible, name])
 
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index 1cc0a5fe9..f80ade35c 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -29,6 +29,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
 
   useEffect(() => {
     if (visible) {
+      store.shell.setMinimalShellMode(false)
       autocompleteView.setup()
       textInput.current?.focus()
       store.nav.setTitle(navIdx, `Search`)
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 599bb5793..a1281e0d7 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -18,6 +18,7 @@ export const Settings = observer(function Settings({
     if (!visible) {
       return
     }
+    store.shell.setMinimalShellMode(false)
     store.nav.setTitle(navIdx, 'Settings')
   }, [visible, store])
 
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 4567ab67a..bc1343aa3 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -116,6 +116,7 @@ export const MobileShell: React.FC = observer(() => {
   const winDim = useWindowDimensions()
   const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
   const swipeGestureInterp = useAnimatedValue(0)
+  const minimalShellInterp = useAnimatedValue(0)
   const tabMenuInterp = useAnimatedValue(0)
   const newTabInterp = useAnimatedValue(0)
   const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false)
@@ -156,6 +157,27 @@ export const MobileShell: React.FC = observer(() => {
   const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive)
   const doNewTab = (url: string) => () => store.nav.newTab(url)
 
+  // minimal shell animation
+  // =
+  useEffect(() => {
+    if (store.shell.minimalShellMode) {
+      Animated.timing(minimalShellInterp, {
+        toValue: 1,
+        duration: 100,
+        useNativeDriver: true,
+      }).start()
+    } else {
+      Animated.timing(minimalShellInterp, {
+        toValue: 0,
+        duration: 100,
+        useNativeDriver: true,
+      }).start()
+    }
+  }, [minimalShellInterp, store.shell.minimalShellMode])
+  const footerMinimalShellTransform = {
+    transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
+  }
+
   // tab selector animation
   // =
   const toggleTabsMenu = (active: boolean) => {
@@ -182,7 +204,7 @@ export const MobileShell: React.FC = observer(() => {
         useNativeDriver: false,
       }).start()
     }
-  }, [isTabsSelectorActive])
+  }, [tabMenuInterp, isTabsSelectorActive])
 
   // new tab animation
   // =
@@ -190,7 +212,7 @@ export const MobileShell: React.FC = observer(() => {
     if (screenRenderDesc.hasNewTab && !isRunningNewTabAnim) {
       setIsRunningNewTabAnim(true)
     }
-  }, [screenRenderDesc.hasNewTab])
+  }, [isRunningNewTabAnim, screenRenderDesc.hasNewTab])
   useEffect(() => {
     if (isRunningNewTabAnim) {
       const reset = () => {
@@ -208,7 +230,7 @@ export const MobileShell: React.FC = observer(() => {
     } else {
       newTabInterp.setValue(0)
     }
-  }, [isRunningNewTabAnim])
+  }, [newTabInterp, store.nav.tab, isRunningNewTabAnim])
 
   // navigation swipes
   // =
@@ -396,10 +418,11 @@ export const MobileShell: React.FC = observer(() => {
         tabMenuInterp={tabMenuInterp}
         onClose={() => toggleTabsMenu(false)}
       />
-      <View
+      <Animated.View
         style={[
           styles.bottomBar,
           {paddingBottom: clamp(safeAreaInsets.bottom, 15, 40)},
+          footerMinimalShellTransform,
         ]}>
         <Btn
           icon={isAtHome ? 'home-solid' : 'home'}
@@ -419,7 +442,7 @@ export const MobileShell: React.FC = observer(() => {
           onLongPress={TABS_ENABLED ? doNewTab('/notifications') : undefined}
           notificationCount={store.me.notificationCount}
         />
-      </View>
+      </Animated.View>
       <Modal />
       <Lightbox />
       <Composer
@@ -565,6 +588,10 @@ const styles = StyleSheet.create({
     paddingHorizontal: 6,
   },
   bottomBar: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
     flexDirection: 'row',
     backgroundColor: colors.white,
     borderTopWidth: 1,