diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-12-12 16:04:14 -0600 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-12-12 16:04:14 -0600 |
commit | 1aec0ee156daa5a1d3e4ead70caf667edb75eebb (patch) | |
tree | e4931885b9dfea5996cad11e47e59f64109d4761 /src | |
parent | 470f444eed0d9643612bbdb9533cd64614834c69 (diff) | |
download | voidsky-1aec0ee156daa5a1d3e4ead70caf667edb75eebb.tar.zst |
Hide footer on scroll down (minimal shell mode)
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/shell-ui.ts | 5 | ||||
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 11 | ||||
-rw-r--r-- | src/view/lib/useOnMainScroll.ts | 25 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 3 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 8 | ||||
-rw-r--r-- | src/view/screens/PostDownvotedBy.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/PostRepostedBy.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/PostThread.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 3 | ||||
-rw-r--r-- | src/view/screens/ProfileFollowers.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/ProfileFollows.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/ProfileMembers.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/Search.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 1 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 37 |
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, |