diff --git a/src/App.native.tsx b/src/App.native.tsx
index 81d4a870e9..fccb2fd414 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -204,38 +204,37 @@ 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 b706774fdc..6b2535411a 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -4,7 +4,10 @@ import './style.css'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
-import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {
+ initialWindowMetrics,
+ SafeAreaProvider,
+} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as Sentry from '@sentry/react-native'
@@ -129,18 +132,16 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -183,31 +184,33 @@ 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 5891ca863a..79b3d5e14a 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,20 +1,25 @@
import {forwardRef, memo, useContext, useMemo} from 'react'
-import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native'
+import {
+ ScrollView,
+ type ScrollViewProps,
+ StyleSheet,
+ View,
+ type ViewProps,
+ type ViewStyle,
+} from 'react-native'
import {type StyleProp} from 'react-native'
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 {isWeb} from '#/platform/detection'
+import {isIOS, isWeb} from '#/platform/detection'
import {useShellLayout} from '#/state/shell/shell-layout'
import {
+ android,
atoms as a,
+ ios,
useBreakpoints,
useLayoutBreakpoints,
useTheme,
@@ -52,9 +57,7 @@ export const Screen = memo(function Screen({
)
})
-export type ContentProps = AnimatedScrollViewProps & {
- style?: StyleProp
- contentContainerStyle?: StyleProp
+export type ContentProps = ScrollViewProps & {
ignoreTabletLayoutOffset?: boolean
}
@@ -62,7 +65,7 @@ export type ContentProps = AnimatedScrollViewProps & {
* Default scroll view for simple pages
*/
export const Content = memo(
- forwardRef(function Content(
+ forwardRef(function Content(
{
children,
style,
@@ -74,39 +77,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 (
-
{isWeb ? (
- {/* @ts-expect-error web only -esb */}
{children}
) : (
children
)}
-
+
)
}),
)
@@ -115,9 +112,6 @@ const scrollViewStyles = StyleSheet.create({
common: {
width: '100%',
},
- contentContainer: {
- paddingBottom: 100,
- },
})
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
@@ -136,11 +130,14 @@ export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
contentContainerStyle,
...props
}: KeyboardAwareContentProps) {
+ const {footerHeight} = useShellLayout()
return (
{
'worklet'
keyboardHeight.set(e.height)
- if (e.height > footerHeight.get()) {
+ if (e.height > footerHeight) {
scrollTo(flatListRef, 0, 1e7, false)
}
},
onEnd: e => {
'worklet'
keyboardHeight.set(e.height)
- if (e.height > footerHeight.get()) {
+ if (e.height > footerHeight) {
scrollTo(flatListRef, 0, 1e7, false)
}
keyboardIsOpening.set(false)
@@ -296,13 +296,11 @@ export function MessagesList({
)
const animatedListStyle = useAnimatedStyle(() => ({
- marginBottom: Math.max(keyboardHeight.get(), footerHeight.get()),
+ marginBottom: Math.max(keyboardHeight.get(), footerHeight),
}))
const animatedStickyViewStyle = useAnimatedStyle(() => ({
- transform: [
- {translateY: -Math.max(keyboardHeight.get(), footerHeight.get())},
- ],
+ transform: [{translateY: -Math.max(keyboardHeight.get(), footerHeight)}],
}))
// -- Message sending
@@ -446,6 +444,8 @@ export function MessagesList({
ListHeaderComponent={
}
+ contentInset={{bottom: 0}}
+ contentContainerStyle={{paddingBottom: 0}}
/>
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index 350fc41aee..7b975d970a 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -1,6 +1,5 @@
import {useCallback, useMemo, useRef, useState} from 'react'
-import {useWindowDimensions, View} from 'react-native'
-import Animated, {useAnimatedStyle} from 'react-native-reanimated'
+import {type LayoutChangeEvent, useWindowDimensions, View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
@@ -50,6 +49,8 @@ export function PostThread({uri}: {uri: string}) {
const anchorPostSource = useUnstablePostSource(uri)
const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
+ const [promptHeight, setPromptHeight] = useState(43)
+
/*
* One query to rule them all
*/
@@ -485,6 +486,7 @@ export function PostThread({uri}: {uri: string}) {
)
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
+ const hasPrompt = !gtMobile && canReply && hasSession
return (
<>
@@ -553,29 +555,37 @@ export function PostThread({uri}: {uri: string}) {
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
+ 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 39f69cfff9..5279c8cf1d 100644
--- a/src/state/shell/shell-layout.tsx
+++ b/src/state/shell/shell-layout.tsx
@@ -1,12 +1,18 @@
-import React from 'react'
-import {SharedValue, useSharedValue} from 'react-native-reanimated'
+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 = React.createContext({
+const LayoutContext = createContext({
headerHeight: {
value: 0,
addListener() {},
@@ -17,33 +23,45 @@ const stateContext = React.createContext({
},
set() {},
},
- footerHeight: {
- value: 0,
- addListener() {},
- removeListener() {},
- modify() {},
- get() {
- return 0
- },
- set() {},
- },
+ footerHeight: 0,
+ setFooterHeight: () => {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const headerHeight = useSharedValue(0)
- const footerHeight = useSharedValue(0)
-
- const value = React.useMemo(
- () => ({
- headerHeight,
- footerHeight,
+ 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,
}),
- [headerHeight, footerHeight],
)
- return {children}
+ const value = useMemo(
+ () => ({
+ headerHeight,
+ footerHeight: isWeb && gtMobile ? 0 : footerHeight,
+ setFooterHeight: (height: number) => setFooterHeight(round4dp(height)),
+ }),
+ [headerHeight, footerHeight, setFooterHeight, gtMobile],
+ )
+
+ return (
+ {children}
+ )
}
export function useShellLayout() {
- return React.useContext(stateContext)
+ return useContext(LayoutContext)
+}
+
+function round4dp(value: number) {
+ return Math.round(value * 10000) / 10000
}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 94cc04f542..63ea673d29 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,6 +1,6 @@
import React, {memo, useRef, useState} from 'react'
import {useWindowDimensions, View} from 'react-native'
-import {runOnJS, useAnimatedStyle} from 'react-native-reanimated'
+import {runOnJS} from 'react-native-reanimated'
import Animated from 'react-native-reanimated'
import {
AppBskyFeedDefs,
@@ -735,14 +735,9 @@ ThreadMenu = memo(ThreadMenu)
function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
const {footerHeight} = useShellLayout()
- const animatedStyle = useAnimatedStyle(() => {
- return {
- bottom: footerHeight.get(),
- }
- })
-
return (
-
+
)
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 7dd04bab3a..180925d58b 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -11,9 +11,10 @@ import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIX
import {useDedupe} from '#/lib/hooks/useDedupe'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {addStyle} from '#/lib/styles'
-import {isIOS} from '#/platform/detection'
+import {isAndroid, isIOS} from '#/platform/detection'
import {useLightbox} from '#/state/lightbox'
-import {useTheme} from '#/alf'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {ios, useTheme} from '#/alf'
import {FlatList_INTERNAL} from './Views'
export type ListMethods = FlatList_INTERNAL
@@ -38,6 +39,7 @@ export type ListProps = Omit<
disableFullWindowScroll?: boolean
sideBorders?: boolean
progressViewOffset?: number
+ footerExtensionHeight?: number
}
export type ListRef = React.MutableRefObject
@@ -54,6 +56,8 @@ let List = React.forwardRef(
style,
progressViewOffset,
automaticallyAdjustsScrollIndicatorInsets = false,
+ contentContainerStyle,
+ footerExtensionHeight = 0,
...props
},
ref,
@@ -62,6 +66,7 @@ let List = React.forwardRef(
const t = useTheme()
const dedupe = useDedupe(400)
const {activeLightbox} = useLightbox()
+ const {footerHeight} = useShellLayout()
function handleScrolledDownChange(didScrollDown: boolean) {
onScrolledDownChange?.(didScrollDown)
@@ -156,14 +161,26 @@ let List = React.forwardRef(
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
{...props}
+ contentContainerStyle={[
+ isAndroid && {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 01aa4afc42..8d83c3c12b 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -60,7 +60,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const pal = usePalette('default')
const {_} = useLingui()
const safeAreaInsets = useSafeAreaInsets()
- const {footerHeight} = useShellLayout()
+ const {setFooterHeight} = useShellLayout()
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} =
useNavigationTabState()
const numUnreadNotifications = useUnreadNotifications()
@@ -152,9 +152,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={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
{hasSession ? (
<>