Hide footer on scroll down (minimal shell mode)
parent
470f444eed
commit
1aec0ee156
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -18,6 +18,7 @@ export const Settings = observer(function Settings({
|
|||
if (!visible) {
|
||||
return
|
||||
}
|
||||
store.shell.setMinimalShellMode(false)
|
||||
store.nav.setTitle(navIdx, 'Settings')
|
||||
}, [visible, store])
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue