diff --git a/src/App.native.tsx b/src/App.native.tsx
index 5de9f82b7b..9843e2905f 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -227,40 +227,39 @@ 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 abf188ea28..d494fbd747 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -3,7 +3,10 @@ import '#/view/icons'
import './style.css'
import {Fragment, useEffect, useState} from 'react'
-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'
@@ -143,20 +146,18 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -201,33 +202,35 @@ 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 ae01c05b99..ba513fa93f 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,19 +1,24 @@
import {forwardRef, memo, useContext, useMemo} from 'react'
-import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native'
-import {type StyleProp} from 'react-native'
+import {
+ ScrollView,
+ type ScrollViewProps,
+ type StyleProp,
+ StyleSheet,
+ View,
+ type ViewProps,
+ type ViewStyle,
+} 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 {useShellLayout} from '#/state/shell/shell-layout'
import {
+ android,
atoms as a,
+ ios,
useBreakpoints,
useLayoutBreakpoints,
useTheme,
@@ -22,7 +27,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'
@@ -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 (
-
{IS_WEB ? (
- {/* @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 77f79b8fb8..84d0b38ba8 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -1,6 +1,5 @@
import {useCallback, useEffect, 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'
@@ -60,6 +59,8 @@ export function PostThread({uri}: {uri: string}) {
hasSession,
)
+ const [promptHeight, setPromptHeight] = useState(43)
+
/*
* One query to rule them all
*/
@@ -519,6 +520,7 @@ export function PostThread({uri}: {uri: string}) {
)
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
+ const hasPrompt = !gtMobile && canReply && hasSession
return (
@@ -604,29 +606,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 f23227bd20..8bcede54a5 100644
--- a/src/state/shell/shell-layout.tsx
+++ b/src/state/shell/shell-layout.tsx
@@ -1,12 +1,18 @@
-import React 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 = React.createContext({
+const LayoutContext = createContext({
headerHeight: {
value: 0,
addListener() {},
@@ -17,34 +23,46 @@ const stateContext = React.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 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/util/List.tsx b/src/view/com/util/List.tsx
index c8c4007f02..ecd774fa77 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -13,8 +13,9 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {addStyle} from '#/lib/styles'
import {useLightbox} from '#/state/lightbox'
-import {useTheme} from '#/alf'
-import {IS_IOS} from '#/env'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {ios, useTheme} from '#/alf'
+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 7c2a010ab1..092caca8c3 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -59,7 +59,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()
@@ -151,9 +151,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 ? (
<>