Sync the bottom bar to screen transitions and scroll
## Summary - Hook the bottom bar up to react-native-screens' transition progress. Every `Layout.Screen` now exposes a `presence` shared value (via `useScreenPresence`) that follows the native push/pop/swipe frame by frame, so screens with `minimalShell` hide and reveal the bar in sync with the transition instead of after it. Fixes the delayed tab bar when leaving a conversation. - Replace the minimal-shell and hide-border refcounts with a small registry that sums 0..1 contributions on the UI thread. The bottom bar border now fades with the screen rather than toggling. - Bring back the scroll-linked bottom bar on Home (removed in #5203). The header and footer share the same drag-linked value, and the bar is revealed on feed change, blur, and app foreground so badges aren't missed. Web included. ## Test plan - [ ] Swipe back from a conversation: bar tracks the swipe, including a cancelled swipe - [ ] Back button from a conversation: bar animates with the pop - [ ] Push a profile over a conversation and pop back - [ ] Post thread: bottom bar border fades in/out with the screen - [ ] Home: scroll down hides header and bar together, scroll up shows them - [ ] Home: scrolled down, swipe into a convo and back - no jump - [ ] Home: bar reappears on feed switch, on leaving Home, and on foregrounding the app - [ ] Deep link / notification into a conversation from another tab - [ ] Mobile web: bar hides/shows on scroll and on conversation screen - [ ] Rapid swipe-back and swipe-back while scrolling (software-mansion/react-native-reanimated#9402, software-mansion/react-native-screens#2659) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWQciPDiipBhqiW922Nivs
This commit is contained in:
+27
-24
@@ -28,6 +28,7 @@ import {
|
|||||||
notificationToURL,
|
notificationToURL,
|
||||||
storePayloadForAccountSwitch,
|
storePayloadForAccountSwitch,
|
||||||
} from '#/lib/hooks/useNotificationHandler'
|
} from '#/lib/hooks/useNotificationHandler'
|
||||||
|
import {ScreenTransitionProvider} from '#/lib/hooks/useScreenPresence'
|
||||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||||
import {useCallOnce} from '#/lib/once'
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {buildStateObject, getCurrentRoute} from '#/lib/routes/helpers'
|
import {buildStateObject, getCurrentRoute} from '#/lib/routes/helpers'
|
||||||
@@ -1052,30 +1053,32 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer
|
<ScreenTransitionProvider>
|
||||||
ref={navigationRef}
|
<NavigationContainer
|
||||||
linking={LINKING}
|
ref={navigationRef}
|
||||||
theme={theme}
|
linking={LINKING}
|
||||||
onStateChange={() => {
|
theme={theme}
|
||||||
const currentScreen = getCurrentRouteName()
|
onStateChange={() => {
|
||||||
// do this before metric
|
const currentScreen = getCurrentRouteName()
|
||||||
setNavigationMetadata({
|
// do this before metric
|
||||||
previousScreen: previousScreen.current,
|
setNavigationMetadata({
|
||||||
currentScreen,
|
previousScreen: previousScreen.current,
|
||||||
})
|
currentScreen,
|
||||||
ax.metric('router:navigate', {from: previousScreen.current})
|
})
|
||||||
previousScreen.current = currentScreen
|
ax.metric('router:navigate', {from: previousScreen.current})
|
||||||
}}
|
previousScreen.current = currentScreen
|
||||||
onReady={onNavigationReady}
|
}}
|
||||||
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
onReady={onNavigationReady}
|
||||||
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
||||||
// See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
|
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
||||||
// I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
|
// See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
|
||||||
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
// I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
|
||||||
// -sfn
|
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
||||||
navigationInChildEnabled>
|
// -sfn
|
||||||
{children}
|
navigationInChildEnabled>
|
||||||
</NavigationContainer>
|
{children}
|
||||||
|
</NavigationContainer>
|
||||||
|
</ScreenTransitionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Animated, {
|
|||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
|
import {ScreenPresenceProvider} from '#/lib/hooks/useScreenPresence'
|
||||||
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
@@ -52,10 +53,9 @@ export const Screen = memo(function Screen({
|
|||||||
const {top} = useSafeAreaInsets()
|
const {top} = useSafeAreaInsets()
|
||||||
const {isWithinSplitView} = useIsWithinSplitView()
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ScreenPresenceProvider>
|
||||||
|
<MinimalShell enabled={minimalShell} />
|
||||||
{IS_WEB && !isWithinSplitView && <WebCenterBorders />}
|
{IS_WEB && !isWithinSplitView && <WebCenterBorders />}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -66,10 +66,19 @@ export const Screen = memo(function Screen({
|
|||||||
]}
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</>
|
</ScreenPresenceProvider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendered inside the presence provider so the shell follows this screen's
|
||||||
|
* transition rather than its focus state.
|
||||||
|
*/
|
||||||
|
function MinimalShell({enabled}: {enabled: boolean}) {
|
||||||
|
useEnableMinimalShellModeForScreen({enabled})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export type ContentProps = AnimatedScrollViewProps & {
|
export type ContentProps = AnimatedScrollViewProps & {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
contentContainerStyle?: StyleProp<ViewStyle>
|
contentContainerStyle?: StyleProp<ViewStyle>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {useCallback, useState} from 'react'
|
||||||
|
import {
|
||||||
|
clamp,
|
||||||
|
Reanimated3DefaultSpringConfig,
|
||||||
|
type SharedValue,
|
||||||
|
useDerivedValue,
|
||||||
|
withSpring,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {scheduleOnRN} from 'react-native-worklets'
|
||||||
|
|
||||||
|
type Contribution = {
|
||||||
|
id: number
|
||||||
|
value: SharedValue<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextId = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sums any number of 0..1 shared values (plus an optional base value) into a
|
||||||
|
* single derived value clamped to 0..1, entirely on the UI thread.
|
||||||
|
*
|
||||||
|
* This replaces boolean refcounting for shell state: each contributor owns a
|
||||||
|
* shared value it can drive however it likes (a spring, a scroll gesture, a
|
||||||
|
* screen transition), and the sum is what the shell renders. Contributions
|
||||||
|
* are summed rather than maxed so that two overlapping contributors (e.g. one
|
||||||
|
* screen transitioning out while another transitions in) hold the total at 1
|
||||||
|
* instead of dipping in the middle.
|
||||||
|
*/
|
||||||
|
export function useContributionRegistry(base?: SharedValue<number>) {
|
||||||
|
const [contributions, setContributions] = useState<Contribution[]>([])
|
||||||
|
|
||||||
|
const total = useDerivedValue(() => {
|
||||||
|
let sum = base ? base.get() : 0
|
||||||
|
for (const contribution of contributions) {
|
||||||
|
sum += contribution.value.get()
|
||||||
|
}
|
||||||
|
return clamp(sum, 0, 1)
|
||||||
|
}, [contributions, base])
|
||||||
|
|
||||||
|
const remove = useCallback((id: number) => {
|
||||||
|
setContributions(prev => prev.filter(c => c.id !== id))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a contribution and returns a function to remove it. Removal animates
|
||||||
|
* the value to 0 first so that a contributor unmounting while still active
|
||||||
|
* (e.g. a screen removed without a transition) does not snap the total.
|
||||||
|
*/
|
||||||
|
const register = useCallback(
|
||||||
|
(value: SharedValue<number>) => {
|
||||||
|
const id = nextId++
|
||||||
|
setContributions(prev => [...prev, {id, value}])
|
||||||
|
return () => {
|
||||||
|
value.set(
|
||||||
|
withSpring(
|
||||||
|
0,
|
||||||
|
{...Reanimated3DefaultSpringConfig, overshootClamping: true},
|
||||||
|
finished => {
|
||||||
|
if (finished) {
|
||||||
|
scheduleOnRN(remove, id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[remove],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {total, register}
|
||||||
|
}
|
||||||
@@ -1,51 +1,95 @@
|
|||||||
import {createContext, useCallback, useContext, useState} from 'react'
|
import {createContext, useContext, useEffect} from 'react'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {
|
||||||
|
type DerivedValue,
|
||||||
|
interpolateColor,
|
||||||
|
type SharedValue,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
type HideBottomBarBorderSetter = () => () => void
|
import {useContributionRegistry} from '#/lib/hooks/useContributionRegistry'
|
||||||
|
import {useScreenPresence} from '#/lib/hooks/useScreenPresence'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
|
|
||||||
const HideBottomBarBorderContext = createContext<boolean>(false)
|
type Register = (contribution: SharedValue<number>) => () => void
|
||||||
|
|
||||||
|
const HideBottomBarBorderContext = createContext<DerivedValue<number> | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
HideBottomBarBorderContext.displayName = 'HideBottomBarBorderContext'
|
HideBottomBarBorderContext.displayName = 'HideBottomBarBorderContext'
|
||||||
const HideBottomBarBorderSetterContext =
|
const HideBottomBarBorderSetterContext = createContext<Register | null>(null)
|
||||||
createContext<HideBottomBarBorderSetter | null>(null)
|
|
||||||
HideBottomBarBorderSetterContext.displayName =
|
HideBottomBarBorderSetterContext.displayName =
|
||||||
'HideBottomBarBorderSetterContext'
|
'HideBottomBarBorderSetterContext'
|
||||||
|
|
||||||
export function useHideBottomBarBorderSetter() {
|
function useHideBottomBarBorderSetter() {
|
||||||
const hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext)
|
const register = useContext(HideBottomBarBorderSetterContext)
|
||||||
if (!hideBottomBarBorder) {
|
if (!register) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider',
|
'useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return hideBottomBarBorder
|
return register
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the bottom bar's top border while the surrounding screen is present,
|
||||||
|
* fading it with the screen transition.
|
||||||
|
*/
|
||||||
export function useHideBottomBarBorderForScreen() {
|
export function useHideBottomBarBorderForScreen() {
|
||||||
const hideBorder = useHideBottomBarBorderSetter()
|
const register = useHideBottomBarBorderSetter()
|
||||||
|
const {presence} = useScreenPresence()
|
||||||
|
const contribution = useSharedValue(0)
|
||||||
|
|
||||||
useFocusEffect(
|
useAnimatedReaction(
|
||||||
useCallback(() => {
|
() => presence.get(),
|
||||||
const cleanup = hideBorder()
|
(current, previous) => {
|
||||||
return () => cleanup()
|
if (current !== previous) {
|
||||||
}, [hideBorder]),
|
contribution.set(current)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => register(contribution), [register, contribution])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How hidden the bottom bar border is, 0 (visible) to 1 (hidden).
|
||||||
|
*/
|
||||||
export function useHideBottomBarBorder() {
|
export function useHideBottomBarBorder() {
|
||||||
return useContext(HideBottomBarBorderContext)
|
const value = useContext(HideBottomBarBorderContext)
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(
|
||||||
|
'useHideBottomBarBorder must be used within a HideBottomBarBorderProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated border color for the bottom bar, blending the border into the
|
||||||
|
* background as screens that hide it come and go.
|
||||||
|
*/
|
||||||
|
export function useBottomBarBorderStyle() {
|
||||||
|
const t = useTheme()
|
||||||
|
const hideBorder = useHideBottomBarBorder()
|
||||||
|
const visibleColor = t.atoms.border_contrast_low.borderColor
|
||||||
|
const hiddenColor = t.atoms.bg.backgroundColor
|
||||||
|
return useAnimatedStyle(() => ({
|
||||||
|
borderColor: interpolateColor(
|
||||||
|
hideBorder.get(),
|
||||||
|
[0, 1],
|
||||||
|
[visibleColor, hiddenColor],
|
||||||
|
),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
const [refCount, setRefCount] = useState(0)
|
const {total, register} = useContributionRegistry()
|
||||||
|
|
||||||
const setter = useCallback(() => {
|
|
||||||
setRefCount(prev => prev + 1)
|
|
||||||
return () => setRefCount(prev => prev - 1)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HideBottomBarBorderSetterContext.Provider value={setter}>
|
<HideBottomBarBorderSetterContext.Provider value={register}>
|
||||||
<HideBottomBarBorderContext.Provider value={refCount > 0}>
|
<HideBottomBarBorderContext.Provider value={total}>
|
||||||
{children}
|
{children}
|
||||||
</HideBottomBarBorderContext.Provider>
|
</HideBottomBarBorderContext.Provider>
|
||||||
</HideBottomBarBorderSetterContext.Provider>
|
</HideBottomBarBorderSetterContext.Provider>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {createContext, useContext} from 'react'
|
||||||
|
import {type SharedValue, useSharedValue} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
export type ScreenPresence = {
|
||||||
|
/**
|
||||||
|
* How visible this screen is on screen, 0..1, tracking the native stack
|
||||||
|
* transition frame by frame (including interactive swipe-back). Always 1 on
|
||||||
|
* web and for screens that are not inside a native stack.
|
||||||
|
*/
|
||||||
|
visibility: SharedValue<number>
|
||||||
|
/**
|
||||||
|
* `visibility`, but also 0 when the screen's tab (or any other ancestor
|
||||||
|
* navigator) is not focused. This is the value shell UI should follow: it is
|
||||||
|
* 1 exactly when the screen is what the user is looking at.
|
||||||
|
*/
|
||||||
|
presence: SharedValue<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScreenPresenceContext = createContext<ScreenPresence | null>(null)
|
||||||
|
ScreenPresenceContext.displayName = 'ScreenPresenceContext'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the presence of the nearest `Layout.Screen`. Outside of one, both
|
||||||
|
* values are a constant 1.
|
||||||
|
*/
|
||||||
|
export function useScreenPresence(): ScreenPresence {
|
||||||
|
const context = useContext(ScreenPresenceContext)
|
||||||
|
const fallback = useSharedValue(1)
|
||||||
|
return context ?? {visibility: fallback, presence: fallback}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import {useContext, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
Reanimated3DefaultSpringConfig,
|
||||||
|
type SharedValue,
|
||||||
|
useDerivedValue,
|
||||||
|
useSharedValue,
|
||||||
|
withSpring,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {
|
||||||
|
ReanimatedScreenProvider,
|
||||||
|
useReanimatedTransitionProgress,
|
||||||
|
} from 'react-native-screens/reanimated'
|
||||||
|
import {
|
||||||
|
NavigationContext,
|
||||||
|
type NavigationProp,
|
||||||
|
type ParamListBase,
|
||||||
|
} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {ScreenPresenceContext} from './context'
|
||||||
|
|
||||||
|
export {type ScreenPresence, useScreenPresence} from './context'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps react-native-screens' screen implementation for one that pipes the
|
||||||
|
* native transition progress event into Reanimated shared values on the UI
|
||||||
|
* thread. Must wrap the navigation tree.
|
||||||
|
*/
|
||||||
|
export function ScreenTransitionProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return <ReanimatedScreenProvider>{children}</ReanimatedScreenProvider>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides `useScreenPresence()` to a screen's subtree. Rendered by
|
||||||
|
* `Layout.Screen`.
|
||||||
|
*/
|
||||||
|
export function ScreenPresenceProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const navigation = useContext(NavigationContext)
|
||||||
|
/*
|
||||||
|
* The transition progress context only exists for routes rendered by a
|
||||||
|
* native stack navigator, and the hook that reads it throws otherwise. Every
|
||||||
|
* stack navigator in the app is a native stack, so the navigator type tells
|
||||||
|
* us whether it is safe to read.
|
||||||
|
*/
|
||||||
|
const isNativeStackRoute = navigation?.getState().type === 'stack'
|
||||||
|
|
||||||
|
if (isNativeStackRoute) {
|
||||||
|
return (
|
||||||
|
<NativeStackScreenPresenceProvider navigation={navigation}>
|
||||||
|
{children}
|
||||||
|
</NativeStackScreenPresenceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <StaticScreenPresenceProvider>{children}</StaticScreenPresenceProvider>
|
||||||
|
}
|
||||||
|
|
||||||
|
function NativeStackScreenPresenceProvider({
|
||||||
|
navigation,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
navigation: NavigationProp<ParamListBase>
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
/*
|
||||||
|
* react-native-screens types these with `Animated.SharedValue`, a Reanimated
|
||||||
|
* 3 alias that no longer exists in Reanimated 4, so the values come through
|
||||||
|
* untyped.
|
||||||
|
*/
|
||||||
|
const {progress, closing} = useReanimatedTransitionProgress() as {
|
||||||
|
progress: SharedValue<number>
|
||||||
|
closing: SharedValue<number>
|
||||||
|
}
|
||||||
|
const ancestorsFocused = useAncestorsFocused(navigation)
|
||||||
|
|
||||||
|
const visibility = useDerivedValue(() => {
|
||||||
|
const value = progress.get()
|
||||||
|
return closing.get() ? 1 - value : value
|
||||||
|
})
|
||||||
|
const presence = useDerivedValue(() =>
|
||||||
|
Math.min(visibility.get(), ancestorsFocused.get()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenPresenceContext.Provider value={{visibility, presence}}>
|
||||||
|
{children}
|
||||||
|
</ScreenPresenceContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StaticScreenPresenceProvider({children}: {children: React.ReactNode}) {
|
||||||
|
const one = useSharedValue(1)
|
||||||
|
return (
|
||||||
|
<ScreenPresenceContext.Provider value={{visibility: one, presence: one}}>
|
||||||
|
{children}
|
||||||
|
</ScreenPresenceContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether every navigator above this screen has it in focus, as a 0/1 shared
|
||||||
|
* value that springs between the two. Stack transitions are covered by the
|
||||||
|
* transition progress, so this only needs to react to changes that do not
|
||||||
|
* animate a native stack, i.e. switching tabs.
|
||||||
|
*/
|
||||||
|
function useAncestorsFocused(navigation: NavigationProp<ParamListBase>) {
|
||||||
|
const focused = useSharedValue(areAncestorsFocused(navigation) ? 1 : 0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ancestors = getAncestors(navigation)
|
||||||
|
if (ancestors.length === 0) return
|
||||||
|
const update = () => {
|
||||||
|
focused.set(
|
||||||
|
withSpring(areAncestorsFocused(navigation) ? 1 : 0, {
|
||||||
|
...Reanimated3DefaultSpringConfig,
|
||||||
|
overshootClamping: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
const unsubscribes = ancestors.flatMap(ancestor => [
|
||||||
|
ancestor.addListener('focus', update),
|
||||||
|
ancestor.addListener('blur', update),
|
||||||
|
])
|
||||||
|
return () => unsubscribes.forEach(unsubscribe => unsubscribe())
|
||||||
|
}, [navigation, focused])
|
||||||
|
|
||||||
|
return focused
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestors(navigation: NavigationProp<ParamListBase>) {
|
||||||
|
const ancestors: NavigationProp<ParamListBase>[] = []
|
||||||
|
let parent = navigation.getParent()
|
||||||
|
while (parent) {
|
||||||
|
ancestors.push(parent)
|
||||||
|
parent = parent.getParent()
|
||||||
|
}
|
||||||
|
return ancestors
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAncestorsFocused(navigation: NavigationProp<ParamListBase>) {
|
||||||
|
// isFocused() already walks up the tree, so the nearest ancestor is enough
|
||||||
|
const parent = navigation.getParent()
|
||||||
|
return parent ? parent.isFocused() : true
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
import {
|
||||||
|
Reanimated3DefaultSpringConfig,
|
||||||
|
useSharedValue,
|
||||||
|
withSpring,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {ScreenPresenceContext} from './context'
|
||||||
|
|
||||||
|
export {type ScreenPresence, useScreenPresence} from './context'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op on web: there are no native stack transitions to track.
|
||||||
|
*/
|
||||||
|
export function ScreenTransitionProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On web, screens do not animate in or out, so presence is simply focus, with
|
||||||
|
* a spring so that dependent shell UI still animates.
|
||||||
|
*/
|
||||||
|
export function ScreenPresenceProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const visibility = useSharedValue(1)
|
||||||
|
const presence = useSharedValue(0)
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
const spring = (to: number) =>
|
||||||
|
withSpring(to, {
|
||||||
|
...Reanimated3DefaultSpringConfig,
|
||||||
|
overshootClamping: true,
|
||||||
|
})
|
||||||
|
presence.set(spring(1))
|
||||||
|
return () => presence.set(spring(0))
|
||||||
|
}, [presence]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenPresenceContext.Provider value={{visibility, presence}}>
|
||||||
|
{children}
|
||||||
|
</ScreenPresenceContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,25 +1,29 @@
|
|||||||
|
import {createContext, useContext, useEffect, useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
createContext,
|
type DerivedValue,
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
|
||||||
Reanimated3DefaultSpringConfig,
|
Reanimated3DefaultSpringConfig,
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
|
useAnimatedReaction,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
|
||||||
|
import {useContributionRegistry} from '#/lib/hooks/useContributionRegistry'
|
||||||
|
import {useScreenPresence} from '#/lib/hooks/useScreenPresence'
|
||||||
|
|
||||||
type StateContext = {
|
type StateContext = {
|
||||||
footerMode: SharedValue<number>
|
/**
|
||||||
|
* How hidden the bottom bar is, 0 (fully visible) to 1 (fully hidden). The
|
||||||
|
* sum of `scrollMode` and every active screen contribution, clamped.
|
||||||
|
*/
|
||||||
|
footerMode: DerivedValue<number>
|
||||||
|
/**
|
||||||
|
* The scroll-linked part of `footerMode`, driven by `MainScrollProvider`.
|
||||||
|
*/
|
||||||
|
scrollMode: SharedValue<number>
|
||||||
}
|
}
|
||||||
type SetContext = {
|
type SetContext = {
|
||||||
add: () => void
|
register: (contribution: SharedValue<number>) => () => void
|
||||||
subtract: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateContext = createContext<StateContext | null>(null)
|
const stateContext = createContext<StateContext | null>(null)
|
||||||
@@ -28,50 +32,13 @@ const setContext = createContext<SetContext | null>(null)
|
|||||||
setContext.displayName = 'MinimalModeSetContext'
|
setContext.displayName = 'MinimalModeSetContext'
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const footerMode = useSharedValue(0)
|
const scrollMode = useSharedValue(0)
|
||||||
|
const {total: footerMode, register} = useContributionRegistry(scrollMode)
|
||||||
const setModeWorklet = useCallback(
|
|
||||||
(v: boolean) => {
|
|
||||||
'worklet'
|
|
||||||
footerMode.set(
|
|
||||||
withSpring(v ? 1 : 0, {
|
|
||||||
...Reanimated3DefaultSpringConfig,
|
|
||||||
overshootClamping: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[footerMode],
|
|
||||||
)
|
|
||||||
|
|
||||||
// defaults to "visible", if the count is >0 it gets hidden
|
|
||||||
const countRef = useRef(0)
|
|
||||||
const add = useCallback(() => {
|
|
||||||
// 0 -> 1 = hide
|
|
||||||
if (countRef.current === 0) setModeWorklet(true)
|
|
||||||
|
|
||||||
countRef.current += 1
|
|
||||||
}, [setModeWorklet])
|
|
||||||
const subtract = useCallback(() => {
|
|
||||||
// 1 -> 0 = show
|
|
||||||
if (countRef.current === 1) setModeWorklet(false)
|
|
||||||
|
|
||||||
// count must never go below 0
|
|
||||||
if (countRef.current > 0) countRef.current -= 1
|
|
||||||
}, [setModeWorklet])
|
|
||||||
|
|
||||||
const setters = useMemo(
|
|
||||||
() => ({
|
|
||||||
add,
|
|
||||||
subtract,
|
|
||||||
}),
|
|
||||||
[add, subtract],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
const setters = useMemo(() => ({register}), [register])
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({footerMode, scrollMode}),
|
||||||
footerMode,
|
[footerMode, scrollMode],
|
||||||
}),
|
|
||||||
[footerMode],
|
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<stateContext.Provider value={value}>
|
<stateContext.Provider value={value}>
|
||||||
@@ -89,6 +56,14 @@ export function useMinimalShellMode() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scroll-linked part of the bottom bar's hidden state, for scroll
|
||||||
|
* providers to drive directly. Reset to 0 to reveal the bar.
|
||||||
|
*/
|
||||||
|
export function useMinimalShellScrollMode() {
|
||||||
|
return useMinimalShellMode().scrollMode
|
||||||
|
}
|
||||||
|
|
||||||
export function useMinimalShellModeSetters() {
|
export function useMinimalShellModeSetters() {
|
||||||
const context = useContext(setContext)
|
const context = useContext(setContext)
|
||||||
if (!context)
|
if (!context)
|
||||||
@@ -98,26 +73,49 @@ export function useMinimalShellModeSetters() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the bottom bar for as long as the calling component is mounted and
|
||||||
|
* `enabled`, independent of navigation. Prefer `Layout.Screen`'s
|
||||||
|
* `minimalShell` prop for screens, which tracks the screen transition.
|
||||||
|
*/
|
||||||
export function useEnableMinimalShellMode({enabled} = {enabled: true}) {
|
export function useEnableMinimalShellMode({enabled} = {enabled: true}) {
|
||||||
const setters = useMinimalShellModeSetters()
|
const {register} = useMinimalShellModeSetters()
|
||||||
|
const contribution = useSharedValue(0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled) {
|
if (!enabled) return
|
||||||
setters.add()
|
const unregister = register(contribution)
|
||||||
return () => setters.subtract()
|
contribution.set(
|
||||||
}
|
withSpring(1, {
|
||||||
}, [enabled, setters])
|
...Reanimated3DefaultSpringConfig,
|
||||||
|
overshootClamping: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return unregister
|
||||||
|
}, [enabled, register, contribution])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the bottom bar while the surrounding screen is present, following its
|
||||||
|
* transition in and out frame by frame. Used by `Layout.Screen`.
|
||||||
|
*/
|
||||||
export function useEnableMinimalShellModeForScreen(
|
export function useEnableMinimalShellModeForScreen(
|
||||||
{enabled} = {enabled: true},
|
{enabled} = {enabled: true},
|
||||||
) {
|
) {
|
||||||
const setters = useMinimalShellModeSetters()
|
const {register} = useMinimalShellModeSetters()
|
||||||
useFocusEffect(
|
const {presence} = useScreenPresence()
|
||||||
useCallback(() => {
|
const contribution = useSharedValue(0)
|
||||||
if (enabled) {
|
|
||||||
setters.add()
|
useAnimatedReaction(
|
||||||
return () => setters.subtract()
|
() => presence.get(),
|
||||||
|
(current, previous) => {
|
||||||
|
if (current !== previous) {
|
||||||
|
contribution.set(current)
|
||||||
}
|
}
|
||||||
}, [enabled, setters]),
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return
|
||||||
|
return register(contribution)
|
||||||
|
}, [enabled, register, contribution])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
import {EventEmitter} from 'eventemitter3'
|
import {EventEmitter} from 'eventemitter3'
|
||||||
|
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
|
import {useMinimalShellScrollMode} from '#/state/shell/minimal-mode'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ export function useHomeHeaderTransform() {
|
|||||||
export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||||
const {headerHeight} = useShellLayout()
|
const {headerHeight} = useShellLayout()
|
||||||
const headerMode = useHomeHeaderMode()
|
const headerMode = useHomeHeaderMode()
|
||||||
|
const footerScrollMode = useMinimalShellScrollMode()
|
||||||
const {top: topInset} = useSafeAreaInsets()
|
const {top: topInset} = useSafeAreaInsets()
|
||||||
const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0
|
const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0
|
||||||
const startDragOffset = useSharedValue<number | null>(null)
|
const startDragOffset = useSharedValue<number | null>(null)
|
||||||
@@ -104,14 +106,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
|||||||
const setMode = useCallback(
|
const setMode = useCallback(
|
||||||
(v: boolean) => {
|
(v: boolean) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
headerMode.set(
|
const target = withSpring(v ? 1 : 0, {
|
||||||
withSpring(v ? 1 : 0, {
|
...Reanimated3DefaultSpringConfig,
|
||||||
...Reanimated3DefaultSpringConfig,
|
overshootClamping: true,
|
||||||
overshootClamping: true,
|
})
|
||||||
}),
|
headerMode.set(target)
|
||||||
)
|
footerScrollMode.set(target)
|
||||||
},
|
},
|
||||||
[headerMode],
|
[headerMode, footerScrollMode],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -216,6 +218,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
|||||||
if (newValue !== headerMode.get()) {
|
if (newValue !== headerMode.get()) {
|
||||||
// Manually adjust the value. This won't be (and shouldn't be) animated.
|
// Manually adjust the value. This won't be (and shouldn't be) animated.
|
||||||
headerMode.set(newValue)
|
headerMode.set(newValue)
|
||||||
|
footerScrollMode.set(newValue)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (didJustRestoreScroll.get()) {
|
if (didJustRestoreScroll.get()) {
|
||||||
@@ -239,6 +242,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
|||||||
headerHeight,
|
headerHeight,
|
||||||
headerPinnedHeight,
|
headerPinnedHeight,
|
||||||
headerMode,
|
headerMode,
|
||||||
|
footerScrollMode,
|
||||||
setMode,
|
setMode,
|
||||||
startDragOffset,
|
startDragOffset,
|
||||||
startMode,
|
startMode,
|
||||||
|
|||||||
+29
-15
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'
|
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'
|
||||||
import {ActivityIndicator, StyleSheet} from 'react-native'
|
import {ActivityIndicator, AppState, StyleSheet} from 'react-native'
|
||||||
import {
|
import {
|
||||||
Reanimated3DefaultSpringConfig,
|
Reanimated3DefaultSpringConfig,
|
||||||
withSpring,
|
withSpring,
|
||||||
@@ -31,6 +31,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
|||||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
|
import {useMinimalShellScrollMode} from '#/state/shell/minimal-mode'
|
||||||
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
||||||
import {FeedPage} from '#/view/com/feeds/FeedPage'
|
import {FeedPage} from '#/view/com/feeds/FeedPage'
|
||||||
import {HomeHeader} from '#/view/com/home/HomeHeader'
|
import {HomeHeader} from '#/view/com/home/HomeHeader'
|
||||||
@@ -156,20 +157,33 @@ function HomeScreenReady({
|
|||||||
|
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const headerMode = useHomeHeaderMode()
|
const headerMode = useHomeHeaderMode()
|
||||||
const showHeader = useCallback(() => {
|
const footerScrollMode = useMinimalShellScrollMode()
|
||||||
|
const showShell = useCallback(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
headerMode.set(
|
const shown = withSpring(0, {
|
||||||
withSpring(0, {
|
...Reanimated3DefaultSpringConfig,
|
||||||
...Reanimated3DefaultSpringConfig,
|
overshootClamping: true,
|
||||||
overshootClamping: true,
|
})
|
||||||
}),
|
headerMode.set(shown)
|
||||||
)
|
footerScrollMode.set(shown)
|
||||||
}, [headerMode])
|
}, [headerMode, footerScrollMode])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
return () => showHeader()
|
return () => showShell()
|
||||||
}, [showHeader]),
|
}, [showShell]),
|
||||||
|
)
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
// Reveal the bars on foreground so you don't miss notifications or messages.
|
||||||
|
const listener = AppState.addEventListener('change', nextAppState => {
|
||||||
|
if (nextAppState === 'active') {
|
||||||
|
showShell()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => listener.remove()
|
||||||
|
}, [showShell]),
|
||||||
)
|
)
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -187,7 +201,7 @@ function HomeScreenReady({
|
|||||||
|
|
||||||
const onPageSelected = useCallback(
|
const onPageSelected = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
showHeader()
|
showShell()
|
||||||
const maybeFeed = allFeeds[index]
|
const maybeFeed = allFeeds[index]
|
||||||
|
|
||||||
// Mutate the ref before setting state to avoid the imperative syncing effect
|
// Mutate the ref before setting state to avoid the imperative syncing effect
|
||||||
@@ -203,7 +217,7 @@ function HomeScreenReady({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ax, setSelectedFeed, showHeader, allFeeds],
|
[ax, setSelectedFeed, showShell, allFeeds],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressSelected = useCallback(() => {
|
const onPressSelected = useCallback(() => {
|
||||||
@@ -214,10 +228,10 @@ function HomeScreenReady({
|
|||||||
(state: 'idle' | 'dragging' | 'settling') => {
|
(state: 'idle' | 'dragging' | 'settling') => {
|
||||||
'worklet'
|
'worklet'
|
||||||
if (state === 'dragging') {
|
if (state === 'dragging') {
|
||||||
showHeader()
|
showShell()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[showHeader],
|
[showShell],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [demoMode] = useDemoMode()
|
const [demoMode] = useDemoMode()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
|||||||
import {BOTTOM_BAR_AVI} from '#/lib/demo'
|
import {BOTTOM_BAR_AVI} from '#/lib/demo'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||||
import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder'
|
import {useBottomBarBorderStyle} from '#/lib/hooks/useHideBottomBarBorder'
|
||||||
import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
|
import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||||
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
|
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
|
||||||
import {clamp} from '#/lib/numbers'
|
import {clamp} from '#/lib/numbers'
|
||||||
@@ -80,7 +80,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
const accountSwitchControl = useDialogControl()
|
const accountSwitchControl = useDialogControl()
|
||||||
const messagesMenuControl = Menu.useMenuControl()
|
const messagesMenuControl = Menu.useMenuControl()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const hideBorder = useHideBottomBarBorder()
|
const borderStyle = useBottomBarBorderStyle()
|
||||||
const iconWidth = 28
|
const iconWidth = 28
|
||||||
|
|
||||||
const showSignIn = useCallback(() => {
|
const showSignIn = useCallback(() => {
|
||||||
@@ -165,9 +165,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
style={[
|
style={[
|
||||||
styles.bottomBar,
|
styles.bottomBar,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
hideBorder
|
borderStyle,
|
||||||
? {borderColor: t.atoms.bg.backgroundColor}
|
|
||||||
: t.atoms.border_contrast_low,
|
|
||||||
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)},
|
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)},
|
||||||
footerMinimalShellTransform,
|
footerMinimalShellTransform,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
import {useNavigationState} from '@react-navigation/native'
|
import {useNavigationState} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder'
|
import {useBottomBarBorderStyle} from '#/lib/hooks/useHideBottomBarBorder'
|
||||||
import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
|
import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||||
import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
|
import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
@@ -57,7 +57,7 @@ export function BottomBarWeb() {
|
|||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
const hideBorder = useHideBottomBarBorder()
|
const borderStyle = useBottomBarBorderStyle()
|
||||||
const accountSwitchControl = useDialogControl()
|
const accountSwitchControl = useDialogControl()
|
||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||||
const iconWidth = 26
|
const iconWidth = 26
|
||||||
@@ -92,9 +92,7 @@ export function BottomBarWeb() {
|
|||||||
styles.bottomBar,
|
styles.bottomBar,
|
||||||
styles.bottomBarWeb,
|
styles.bottomBarWeb,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
hideBorder
|
borderStyle,
|
||||||
? {borderColor: t.atoms.bg.backgroundColor}
|
|
||||||
: t.atoms.border_contrast_low,
|
|
||||||
footerMinimalShellTransform,
|
footerMinimalShellTransform,
|
||||||
]}
|
]}
|
||||||
onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}>
|
onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user