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:
Claude
2026-09-07 19:40:57 +00:00
parent 08069e2877
commit 2dc7071e7d
12 changed files with 525 additions and 152 deletions
+11 -7
View File
@@ -13,6 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {EventEmitter} from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext'
import {useMinimalShellScrollMode} from '#/state/shell/minimal-mode'
import {useShellLayout} from '#/state/shell/shell-layout'
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
@@ -95,6 +96,7 @@ export function useHomeHeaderTransform() {
export function MainScrollProvider({children}: {children: React.ReactNode}) {
const {headerHeight} = useShellLayout()
const headerMode = useHomeHeaderMode()
const footerScrollMode = useMinimalShellScrollMode()
const {top: topInset} = useSafeAreaInsets()
const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0
const startDragOffset = useSharedValue<number | null>(null)
@@ -104,14 +106,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const setMode = useCallback(
(v: boolean) => {
'worklet'
headerMode.set(
withSpring(v ? 1 : 0, {
...Reanimated3DefaultSpringConfig,
overshootClamping: true,
}),
)
const target = withSpring(v ? 1 : 0, {
...Reanimated3DefaultSpringConfig,
overshootClamping: true,
})
headerMode.set(target)
footerScrollMode.set(target)
},
[headerMode],
[headerMode, footerScrollMode],
)
useEffect(() => {
@@ -216,6 +218,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
if (newValue !== headerMode.get()) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
headerMode.set(newValue)
footerScrollMode.set(newValue)
}
} else {
if (didJustRestoreScroll.get()) {
@@ -239,6 +242,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
headerHeight,
headerPinnedHeight,
headerMode,
footerScrollMode,
setMode,
startDragOffset,
startMode,