From 1aec0ee156daa5a1d3e4ead70caf667edb75eebb Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 12 Dec 2022 16:04:14 -0600 Subject: [PATCH] Hide footer on scroll down (minimal shell mode) --- src/state/models/shell-ui.ts | 5 ++++ src/view/com/notifications/Feed.tsx | 4 +++ src/view/com/posts/Feed.tsx | 4 +++ src/view/com/util/ViewSelector.tsx | 11 +++++++- src/view/lib/useOnMainScroll.ts | 25 ++++++++++++++++++ src/view/screens/Home.tsx | 3 +++ src/view/screens/Notifications.tsx | 8 +++++- src/view/screens/PostDownvotedBy.tsx | 1 + src/view/screens/PostRepostedBy.tsx | 1 + src/view/screens/PostThread.tsx | 1 + src/view/screens/Profile.tsx | 3 +++ src/view/screens/ProfileFollowers.tsx | 1 + src/view/screens/ProfileFollows.tsx | 1 + src/view/screens/ProfileMembers.tsx | 1 + src/view/screens/Search.tsx | 1 + src/view/screens/Settings.tsx | 1 + src/view/shell/mobile/index.tsx | 37 +++++++++++++++++++++++---- 17 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 src/view/lib/useOnMainScroll.ts diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts index 3199e421..01e1caca 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 a6af0f88..c986bca5 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} /> )} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 59b529dc..e3451379 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 scrollElRef?: MutableRefObject | 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 264a9086..e436e41b 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 00000000..ee008122 --- /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, +) => void + +export function useOnMainScroll(store: RootStoreModel) { + let [lastY, setLastY] = useState(0) + let isMinimal = store.shell.minimalShellMode + return function onMainScroll(event: NativeSyntheticEvent) { + 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 a1d20273..078fb980 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(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 ? ( { const store = useStores() + const onMainScroll = useOnMainScroll(store) useEffect(() => { if (!visible) { @@ -33,7 +35,11 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => { return ( - + ) } diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx index b16ec5c0..ab110f8f 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 d8e4b910..4e84617d 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 1e63ac39..4caf144b 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 93a7147b..86be47fb 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(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 b19a5bc3..49b3e2e0 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 e54b562e..58df6e76 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 b4b6c7e5..9d6723fe 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 1cc0a5fe..f80ade35 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 599bb579..a1281e0d 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 4567ab67..bc1343aa 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)} /> - { onLongPress={TABS_ENABLED ? doNewTab('/notifications') : undefined} notificationCount={store.me.notificationCount} /> - +