diff --git a/src/App.native.tsx b/src/App.native.tsx
index 7bbe72a8d1..b4a5f9dae6 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -232,42 +232,41 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 178d3b8298..f1110fcc47 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -4,7 +4,10 @@ import './style.css'
import {Fragment, useEffect, useState} from 'react'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
-import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {
+ initialWindowMetrics,
+ SafeAreaProvider,
+} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react/macro'
import * as Sentry from '@sentry/react-native'
@@ -149,24 +152,22 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -211,37 +212,39 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 43ae23b52b..be183cae1a 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,5 +1,7 @@
import {forwardRef, memo, useContext, useMemo} from 'react'
import {
+ ScrollView,
+ type ScrollViewProps,
type StyleProp,
StyleSheet,
View,
@@ -10,16 +12,14 @@ import {
KeyboardAwareScrollView,
type KeyboardAwareScrollViewProps,
} from 'react-native-keyboard-controller'
-import Animated, {
- type AnimatedScrollViewProps,
- useAnimatedProps,
-} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {
+ android,
atoms as a,
+ ios,
useBreakpoints,
useLayoutBreakpoints,
useTheme,
@@ -28,7 +28,7 @@ import {
import {useDialogContext} from '#/components/Dialog'
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
-import {IS_WEB} from '#/env'
+import {IS_IOS, IS_WEB} from '#/env'
export * from '#/components/Layout/const'
export * as Header from '#/components/Layout/Header'
@@ -63,9 +63,7 @@ export const Screen = memo(function Screen({
)
})
-export type ContentProps = AnimatedScrollViewProps & {
- style?: StyleProp
- contentContainerStyle?: StyleProp
+export type ContentProps = ScrollViewProps & {
ignoreTabletLayoutOffset?: boolean
}
@@ -73,7 +71,7 @@ export type ContentProps = AnimatedScrollViewProps & {
* Default scroll view for simple pages
*/
export const Content = memo(
- forwardRef(function Content(
+ forwardRef(function Content(
{
children,
style,
@@ -85,39 +83,33 @@ export const Content = memo(
) {
const t = useTheme()
const {footerHeight} = useShellLayout()
- const animatedProps = useAnimatedProps(() => {
- return {
- scrollIndicatorInsets: {
- bottom: footerHeight.get(),
- top: 0,
- right: 1,
- },
- } satisfies AnimatedScrollViewProps
- })
return (
-
{IS_WEB ? (
- {/* @ts-expect-error web only -esb */}
{children}
) : (
children
)}
-
+
)
}),
)
@@ -126,9 +118,6 @@ const scrollViewStyles = StyleSheet.create({
common: {
width: '100%',
},
- contentContainer: {
- paddingBottom: 100,
- },
})
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
@@ -147,11 +136,14 @@ export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
contentContainerStyle,
...props
}: KeyboardAwareContentProps) {
+ const {footerHeight} = useShellLayout()
return (
@@ -630,29 +632,37 @@ export function PostThread({uri}: {uri: string}) {
* Default: 50
*/
updateCellsBatchingPeriod={100}
+ footerExtensionHeight={hasPrompt ? promptHeight : 0}
/>
)}
- {!gtMobile && canReply && hasSession && (
-
+ {hasPrompt && (
+
+ setPromptHeight(Math.round(evt.nativeEvent.layout.height))
+ }
+ />
)}
)
}
-function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
+function MobileComposePrompt({
+ onPressReply,
+ onLayout,
+}: {
+ onPressReply: () => unknown
+ onLayout?: (event: LayoutChangeEvent) => void
+}) {
const {footerHeight} = useShellLayout()
- const animatedStyle = useAnimatedStyle(() => {
- return {
- bottom: footerHeight.get(),
- }
- })
-
return (
-
+
-
+
)
}
diff --git a/src/state/shell/shell-layout.tsx b/src/state/shell/shell-layout.tsx
index 9a7e3d3c83..8bcede54a5 100644
--- a/src/state/shell/shell-layout.tsx
+++ b/src/state/shell/shell-layout.tsx
@@ -1,12 +1,18 @@
-import {createContext, useContext, useMemo} from 'react'
+import {createContext, useContext, useMemo, useState} from 'react'
import {type SharedValue, useSharedValue} from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
-type StateContext = {
+import {clamp} from '#/lib/numbers'
+import {isWeb} from '#/platform/detection'
+import {atoms as a, platform, useBreakpoints} from '#/alf'
+
+type LayoutContextValue = {
headerHeight: SharedValue
- footerHeight: SharedValue
+ footerHeight: number
+ setFooterHeight: (height: number) => void
}
-const stateContext = createContext({
+const LayoutContext = createContext({
headerHeight: {
value: 0,
addListener() {},
@@ -17,34 +23,46 @@ const stateContext = createContext({
},
set() {},
},
- footerHeight: {
- value: 0,
- addListener() {},
- removeListener() {},
- modify() {},
- get() {
- return 0
- },
- set() {},
- },
+ footerHeight: 0,
+ setFooterHeight: () => {},
})
-stateContext.displayName = 'ShellLayoutContext'
+LayoutContext.displayName = 'ShellLayoutContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const headerHeight = useSharedValue(0)
- const footerHeight = useSharedValue(0)
+ const insets = useSafeAreaInsets()
+ const {gtMobile} = useBreakpoints()
+ const [footerHeight, setFooterHeight] = useState(() =>
+ platform({
+ // try and precisely guess the footer height, then round it to 4 decimal places
+ // to remove floating point imprecision. if we can guess it exactly,
+ // we get to skip a rerender
+ native: round4dp(
+ 47 + a.border.borderWidth + clamp(insets.bottom, 15, 60),
+ ),
+ web: 58,
+ default: 0,
+ }),
+ )
const value = useMemo(
() => ({
headerHeight,
- footerHeight,
+ footerHeight: isWeb && gtMobile ? 0 : footerHeight,
+ setFooterHeight: (height: number) => setFooterHeight(round4dp(height)),
}),
- [headerHeight, footerHeight],
+ [headerHeight, footerHeight, setFooterHeight, gtMobile],
)
- return {children}
+ return (
+ {children}
+ )
}
export function useShellLayout() {
- return useContext(stateContext)
+ return useContext(LayoutContext)
+}
+
+function round4dp(value: number) {
+ return Math.round(value * 10000) / 10000
}
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index aee397bb79..b65a574520 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -12,9 +12,10 @@ import {useDedupe} from '#/lib/hooks/useDedupe'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {addStyle} from '#/lib/styles'
-import {useTheme} from '#/alf'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {ios, useTheme} from '#/alf'
import {useLightbox} from '#/components/Lightbox/state'
-import {IS_IOS} from '#/env'
+import {IS_ANDROID, IS_IOS} from '#/env'
import {FlatList_INTERNAL} from './Views'
export type ListMethods = FlatList_INTERNAL
@@ -39,6 +40,7 @@ export type ListProps = Omit<
disableFullWindowScroll?: boolean
sideBorders?: boolean
progressViewOffset?: number
+ footerExtensionHeight?: number
}
export type ListRef = React.RefObject
@@ -55,6 +57,8 @@ let List = forwardRef(
style,
progressViewOffset,
automaticallyAdjustsScrollIndicatorInsets = false,
+ contentContainerStyle,
+ footerExtensionHeight = 0,
...props
},
ref,
@@ -63,6 +67,7 @@ let List = forwardRef(
const t = useTheme()
const dedupe = useDedupe(400)
const scrollsToTop = useAllowScrollToTop()
+ const {footerHeight} = useShellLayout()
const handleScrolledDownChange = useNonReactiveCallback(
(didScrollDown: boolean) => {
@@ -159,14 +164,26 @@ let List = forwardRef(
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
{...props}
+ contentContainerStyle={[
+ IS_ANDROID && {paddingBottom: footerHeight + footerExtensionHeight},
+ contentContainerStyle,
+ ]}
automaticallyAdjustsScrollIndicatorInsets={
automaticallyAdjustsScrollIndicatorInsets
}
scrollIndicatorInsets={{
top: headerOffset,
right: 1,
+ bottom: footerHeight + footerExtensionHeight,
...props.scrollIndicatorInsets,
}}
+ contentInset={ios({
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: footerHeight + footerExtensionHeight,
+ ...props.contentInset,
+ })}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
contentOffset={contentOffset}
refreshControl={refreshControl}
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 8c1fb3c52b..3169558a4a 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -61,7 +61,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const t = useTheme()
const {_} = useLingui()
const safeAreaInsets = useSafeAreaInsets()
- const {footerHeight} = useShellLayout()
+ const {setFooterHeight} = useShellLayout()
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} =
useNavigationTabState()
const numUnreadNotifications = useUnreadNotifications()
@@ -154,9 +154,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)},
footerMinimalShellTransform,
]}
- onLayout={e => {
- footerHeight.set(e.nativeEvent.layout.height)
- }}>
+ onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
{hasSession ? (
<>
footerHeight.set(event.nativeEvent.layout.height)}>
+ onLayout={event => setFooterHeight(event.nativeEvent.layout.height)}>
{hasSession ? (
<>