make scrollview automatically adapt to footer height
This commit is contained in:
+30
-31
@@ -204,38 +204,37 @@ function App() {
|
|||||||
* that is set up in the InnerApp component above.
|
* that is set up in the InnerApp component above.
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<GeolocationProvider>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
<A11yProvider>
|
<GeolocationProvider>
|
||||||
<KeyboardControllerProvider>
|
<A11yProvider>
|
||||||
<SessionProvider>
|
<KeyboardControllerProvider>
|
||||||
<PrefsStateProvider>
|
<SessionProvider>
|
||||||
<I18nProvider>
|
<PrefsStateProvider>
|
||||||
<ShellStateProvider>
|
<I18nProvider>
|
||||||
<InvitesStateProvider>
|
<ShellStateProvider>
|
||||||
<ModalStateProvider>
|
<InvitesStateProvider>
|
||||||
<DialogStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<DialogStateProvider>
|
||||||
<PortalProvider>
|
<LightboxStateProvider>
|
||||||
<BottomSheetProvider>
|
<PortalProvider>
|
||||||
<StarterPackProvider>
|
<BottomSheetProvider>
|
||||||
<SafeAreaProvider
|
<StarterPackProvider>
|
||||||
initialMetrics={initialWindowMetrics}>
|
|
||||||
<InnerApp />
|
<InnerApp />
|
||||||
</SafeAreaProvider>
|
</StarterPackProvider>
|
||||||
</StarterPackProvider>
|
</BottomSheetProvider>
|
||||||
</BottomSheetProvider>
|
</PortalProvider>
|
||||||
</PortalProvider>
|
</LightboxStateProvider>
|
||||||
</LightboxStateProvider>
|
</DialogStateProvider>
|
||||||
</DialogStateProvider>
|
</ModalStateProvider>
|
||||||
</ModalStateProvider>
|
</InvitesStateProvider>
|
||||||
</InvitesStateProvider>
|
</ShellStateProvider>
|
||||||
</ShellStateProvider>
|
</I18nProvider>
|
||||||
</I18nProvider>
|
</PrefsStateProvider>
|
||||||
</PrefsStateProvider>
|
</SessionProvider>
|
||||||
</SessionProvider>
|
</KeyboardControllerProvider>
|
||||||
</KeyboardControllerProvider>
|
</A11yProvider>
|
||||||
</A11yProvider>
|
</GeolocationProvider>
|
||||||
</GeolocationProvider>
|
</SafeAreaProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-38
@@ -4,7 +4,10 @@ import './style.css'
|
|||||||
|
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
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 {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
@@ -129,18 +132,16 @@ function InnerApp() {
|
|||||||
<UnreadNotifsProvider>
|
<UnreadNotifsProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<SafeAreaProvider>
|
<ProgressGuideProvider>
|
||||||
<ProgressGuideProvider>
|
<ServiceConfigProvider>
|
||||||
<ServiceConfigProvider>
|
<HideBottomBarBorderProvider>
|
||||||
<HideBottomBarBorderProvider>
|
<IntentDialogProvider>
|
||||||
<IntentDialogProvider>
|
<Shell />
|
||||||
<Shell />
|
<NuxDialogs />
|
||||||
<NuxDialogs />
|
</IntentDialogProvider>
|
||||||
</IntentDialogProvider>
|
</HideBottomBarBorderProvider>
|
||||||
</HideBottomBarBorderProvider>
|
</ServiceConfigProvider>
|
||||||
</ServiceConfigProvider>
|
</ProgressGuideProvider>
|
||||||
</ProgressGuideProvider>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
@@ -183,31 +184,33 @@ function App() {
|
|||||||
* that is set up in the InnerApp component above.
|
* that is set up in the InnerApp component above.
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<GeolocationProvider>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
<A11yProvider>
|
<GeolocationProvider>
|
||||||
<SessionProvider>
|
<A11yProvider>
|
||||||
<PrefsStateProvider>
|
<SessionProvider>
|
||||||
<I18nProvider>
|
<PrefsStateProvider>
|
||||||
<ShellStateProvider>
|
<I18nProvider>
|
||||||
<InvitesStateProvider>
|
<ShellStateProvider>
|
||||||
<ModalStateProvider>
|
<InvitesStateProvider>
|
||||||
<DialogStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<DialogStateProvider>
|
||||||
<PortalProvider>
|
<LightboxStateProvider>
|
||||||
<StarterPackProvider>
|
<PortalProvider>
|
||||||
<InnerApp />
|
<StarterPackProvider>
|
||||||
</StarterPackProvider>
|
<InnerApp />
|
||||||
</PortalProvider>
|
</StarterPackProvider>
|
||||||
</LightboxStateProvider>
|
</PortalProvider>
|
||||||
</DialogStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</DialogStateProvider>
|
||||||
</InvitesStateProvider>
|
</ModalStateProvider>
|
||||||
</ShellStateProvider>
|
</InvitesStateProvider>
|
||||||
</I18nProvider>
|
</ShellStateProvider>
|
||||||
</PrefsStateProvider>
|
</I18nProvider>
|
||||||
</SessionProvider>
|
</PrefsStateProvider>
|
||||||
</A11yProvider>
|
</SessionProvider>
|
||||||
</GeolocationProvider>
|
</A11yProvider>
|
||||||
|
</GeolocationProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import {forwardRef, memo, useContext, useMemo} from 'react'
|
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 {type StyleProp} from 'react-native'
|
||||||
import {
|
import {
|
||||||
KeyboardAwareScrollView,
|
KeyboardAwareScrollView,
|
||||||
type KeyboardAwareScrollViewProps,
|
type KeyboardAwareScrollViewProps,
|
||||||
} from 'react-native-keyboard-controller'
|
} from 'react-native-keyboard-controller'
|
||||||
import Animated, {
|
|
||||||
type AnimatedScrollViewProps,
|
|
||||||
useAnimatedProps,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
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 {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {
|
import {
|
||||||
|
android,
|
||||||
atoms as a,
|
atoms as a,
|
||||||
|
ios,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
useLayoutBreakpoints,
|
useLayoutBreakpoints,
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -52,9 +57,7 @@ export const Screen = memo(function Screen({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ContentProps = AnimatedScrollViewProps & {
|
export type ContentProps = ScrollViewProps & {
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
contentContainerStyle?: StyleProp<ViewStyle>
|
|
||||||
ignoreTabletLayoutOffset?: boolean
|
ignoreTabletLayoutOffset?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ export type ContentProps = AnimatedScrollViewProps & {
|
|||||||
* Default scroll view for simple pages
|
* Default scroll view for simple pages
|
||||||
*/
|
*/
|
||||||
export const Content = memo(
|
export const Content = memo(
|
||||||
forwardRef<Animated.ScrollView, ContentProps>(function Content(
|
forwardRef<ScrollView, ContentProps>(function Content(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
@@ -74,39 +77,33 @@ export const Content = memo(
|
|||||||
) {
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
const animatedProps = useAnimatedProps(() => {
|
|
||||||
return {
|
|
||||||
scrollIndicatorInsets: {
|
|
||||||
bottom: footerHeight.get(),
|
|
||||||
top: 0,
|
|
||||||
right: 1,
|
|
||||||
},
|
|
||||||
} satisfies AnimatedScrollViewProps
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.ScrollView
|
<ScrollView
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id="content"
|
id="content"
|
||||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||||
|
scrollIndicatorInsets={{
|
||||||
|
bottom: footerHeight,
|
||||||
|
top: 0,
|
||||||
|
right: 1,
|
||||||
|
}}
|
||||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||||
// sets the scroll inset to the height of the footer
|
|
||||||
animatedProps={animatedProps}
|
|
||||||
style={[scrollViewStyles.common, style]}
|
style={[scrollViewStyles.common, style]}
|
||||||
|
contentInset={ios({top: 0, left: 0, bottom: footerHeight, right: 0})}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
scrollViewStyles.contentContainer,
|
!isIOS && {paddingBottom: footerHeight},
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
]}
|
]}
|
||||||
{...props}>
|
{...props}>
|
||||||
{isWeb ? (
|
{isWeb ? (
|
||||||
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||||
{/* @ts-expect-error web only -esb */}
|
|
||||||
{children}
|
{children}
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</Animated.ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -115,9 +112,6 @@ const scrollViewStyles = StyleSheet.create({
|
|||||||
common: {
|
common: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
contentContainer: {
|
|
||||||
paddingBottom: 100,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
|
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
|
||||||
@@ -136,11 +130,14 @@ export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
|
|||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
...props
|
...props
|
||||||
}: KeyboardAwareContentProps) {
|
}: KeyboardAwareContentProps) {
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
return (
|
return (
|
||||||
<KeyboardAwareScrollView
|
<KeyboardAwareScrollView
|
||||||
style={[scrollViewStyles.common, style]}
|
style={[scrollViewStyles.common, style]}
|
||||||
|
contentInset={ios({top: 0, left: 0, bottom: footerHeight, right: 0})}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
scrollViewStyles.contentContainer,
|
android({paddingBottom: footerHeight}),
|
||||||
|
web({paddingBottom: footerHeight}),
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
]}
|
]}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
|||||||
@@ -40,11 +40,7 @@ export function useMinimalShellFooterTransform() {
|
|||||||
opacity: Math.pow(1 - footerModeValue, 2),
|
opacity: Math.pow(1 - footerModeValue, 2),
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateY: interpolate(
|
translateY: interpolate(footerModeValue, [0, 1], [0, footerHeight]),
|
||||||
footerModeValue,
|
|
||||||
[0, 1],
|
|
||||||
[0, footerHeight.get()],
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,14 +279,14 @@ export function MessagesList({
|
|||||||
onMove: e => {
|
onMove: e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
keyboardHeight.set(e.height)
|
keyboardHeight.set(e.height)
|
||||||
if (e.height > footerHeight.get()) {
|
if (e.height > footerHeight) {
|
||||||
scrollTo(flatListRef, 0, 1e7, false)
|
scrollTo(flatListRef, 0, 1e7, false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEnd: e => {
|
onEnd: e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
keyboardHeight.set(e.height)
|
keyboardHeight.set(e.height)
|
||||||
if (e.height > footerHeight.get()) {
|
if (e.height > footerHeight) {
|
||||||
scrollTo(flatListRef, 0, 1e7, false)
|
scrollTo(flatListRef, 0, 1e7, false)
|
||||||
}
|
}
|
||||||
keyboardIsOpening.set(false)
|
keyboardIsOpening.set(false)
|
||||||
@@ -296,13 +296,11 @@ export function MessagesList({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const animatedListStyle = useAnimatedStyle(() => ({
|
const animatedListStyle = useAnimatedStyle(() => ({
|
||||||
marginBottom: Math.max(keyboardHeight.get(), footerHeight.get()),
|
marginBottom: Math.max(keyboardHeight.get(), footerHeight),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const animatedStickyViewStyle = useAnimatedStyle(() => ({
|
const animatedStickyViewStyle = useAnimatedStyle(() => ({
|
||||||
transform: [
|
transform: [{translateY: -Math.max(keyboardHeight.get(), footerHeight)}],
|
||||||
{translateY: -Math.max(keyboardHeight.get(), footerHeight.get())},
|
|
||||||
],
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// -- Message sending
|
// -- Message sending
|
||||||
@@ -446,6 +444,8 @@ export function MessagesList({
|
|||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||||
}
|
}
|
||||||
|
contentInset={{bottom: 0}}
|
||||||
|
contentContainerStyle={{paddingBottom: 0}}
|
||||||
/>
|
/>
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
<Animated.View style={animatedStickyViewStyle}>
|
<Animated.View style={animatedStickyViewStyle}>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useCallback, useMemo, useRef, useState} from 'react'
|
import {useCallback, useMemo, useRef, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {type LayoutChangeEvent, useWindowDimensions, View} from 'react-native'
|
||||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
@@ -50,6 +49,8 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
const anchorPostSource = useUnstablePostSource(uri)
|
const anchorPostSource = useUnstablePostSource(uri)
|
||||||
const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
|
const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
|
||||||
|
|
||||||
|
const [promptHeight, setPromptHeight] = useState(43)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* One query to rule them all
|
* One query to rule them all
|
||||||
*/
|
*/
|
||||||
@@ -485,6 +486,7 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
|
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
|
||||||
|
const hasPrompt = !gtMobile && canReply && hasSession
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -553,29 +555,37 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
initialNumToRender={initialNumToRender}
|
initialNumToRender={initialNumToRender}
|
||||||
windowSize={11}
|
windowSize={11}
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
|
footerExtensionHeight={hasPrompt ? promptHeight : 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!gtMobile && canReply && hasSession && (
|
{hasPrompt && (
|
||||||
<MobileComposePrompt onPressReply={onReplyToAnchor} />
|
<MobileComposePrompt
|
||||||
|
onPressReply={onReplyToAnchor}
|
||||||
|
onLayout={evt =>
|
||||||
|
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 {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
bottom: footerHeight.get(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
|
<View
|
||||||
|
style={[a.fixed, a.left_0, a.right_0, {bottom: footerHeight}]}
|
||||||
|
onLayout={onLayout}>
|
||||||
<PostThreadComposePrompt onPressCompose={onPressReply} />
|
<PostThreadComposePrompt onPressCompose={onPressReply} />
|
||||||
</Animated.View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import React from 'react'
|
import {createContext, useContext, useMemo, useState} from 'react'
|
||||||
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
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<number>
|
headerHeight: SharedValue<number>
|
||||||
footerHeight: SharedValue<number>
|
footerHeight: number
|
||||||
|
setFooterHeight: (height: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateContext = React.createContext<StateContext>({
|
const LayoutContext = createContext<LayoutContextValue>({
|
||||||
headerHeight: {
|
headerHeight: {
|
||||||
value: 0,
|
value: 0,
|
||||||
addListener() {},
|
addListener() {},
|
||||||
@@ -17,33 +23,45 @@ const stateContext = React.createContext<StateContext>({
|
|||||||
},
|
},
|
||||||
set() {},
|
set() {},
|
||||||
},
|
},
|
||||||
footerHeight: {
|
footerHeight: 0,
|
||||||
value: 0,
|
setFooterHeight: () => {},
|
||||||
addListener() {},
|
|
||||||
removeListener() {},
|
|
||||||
modify() {},
|
|
||||||
get() {
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
set() {},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const headerHeight = useSharedValue(0)
|
const headerHeight = useSharedValue(0)
|
||||||
const footerHeight = useSharedValue(0)
|
const insets = useSafeAreaInsets()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const value = React.useMemo(
|
const [footerHeight, setFooterHeight] = useState(() =>
|
||||||
() => ({
|
platform({
|
||||||
headerHeight,
|
// try and precisely guess the footer height, then round it to 4 decimal places
|
||||||
footerHeight,
|
// 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 <stateContext.Provider value={value}>{children}</stateContext.Provider>
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
headerHeight,
|
||||||
|
footerHeight: isWeb && gtMobile ? 0 : footerHeight,
|
||||||
|
setFooterHeight: (height: number) => setFooterHeight(round4dp(height)),
|
||||||
|
}),
|
||||||
|
[headerHeight, footerHeight, setFooterHeight, gtMobile],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShellLayout() {
|
export function useShellLayout() {
|
||||||
return React.useContext(stateContext)
|
return useContext(LayoutContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
function round4dp(value: number) {
|
||||||
|
return Math.round(value * 10000) / 10000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {memo, useRef, useState} from 'react'
|
import React, {memo, useRef, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
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 Animated from 'react-native-reanimated'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
@@ -735,14 +735,9 @@ ThreadMenu = memo(ThreadMenu)
|
|||||||
function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
|
function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
bottom: footerHeight.get(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
|
<Animated.View
|
||||||
|
style={[a.fixed, a.left_0, a.right_0, {bottom: footerHeight}]}>
|
||||||
<PostThreadComposePrompt onPressCompose={onPressReply} />
|
<PostThreadComposePrompt onPressCompose={onPressReply} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIX
|
|||||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {addStyle} from '#/lib/styles'
|
import {addStyle} from '#/lib/styles'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isAndroid, isIOS} from '#/platform/detection'
|
||||||
import {useLightbox} from '#/state/lightbox'
|
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'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
|
|
||||||
export type ListMethods = FlatList_INTERNAL
|
export type ListMethods = FlatList_INTERNAL
|
||||||
@@ -38,6 +39,7 @@ export type ListProps<ItemT = any> = Omit<
|
|||||||
disableFullWindowScroll?: boolean
|
disableFullWindowScroll?: boolean
|
||||||
sideBorders?: boolean
|
sideBorders?: boolean
|
||||||
progressViewOffset?: number
|
progressViewOffset?: number
|
||||||
|
footerExtensionHeight?: number
|
||||||
}
|
}
|
||||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||||
|
|
||||||
@@ -54,6 +56,8 @@ let List = React.forwardRef<ListMethods, ListProps>(
|
|||||||
style,
|
style,
|
||||||
progressViewOffset,
|
progressViewOffset,
|
||||||
automaticallyAdjustsScrollIndicatorInsets = false,
|
automaticallyAdjustsScrollIndicatorInsets = false,
|
||||||
|
contentContainerStyle,
|
||||||
|
footerExtensionHeight = 0,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -62,6 +66,7 @@ let List = React.forwardRef<ListMethods, ListProps>(
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const dedupe = useDedupe(400)
|
const dedupe = useDedupe(400)
|
||||||
const {activeLightbox} = useLightbox()
|
const {activeLightbox} = useLightbox()
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
function handleScrolledDownChange(didScrollDown: boolean) {
|
function handleScrolledDownChange(didScrollDown: boolean) {
|
||||||
onScrolledDownChange?.(didScrollDown)
|
onScrolledDownChange?.(didScrollDown)
|
||||||
@@ -156,14 +161,26 @@ let List = React.forwardRef<ListMethods, ListProps>(
|
|||||||
onViewableItemsChanged={onViewableItemsChanged}
|
onViewableItemsChanged={onViewableItemsChanged}
|
||||||
viewabilityConfig={viewabilityConfig}
|
viewabilityConfig={viewabilityConfig}
|
||||||
{...props}
|
{...props}
|
||||||
|
contentContainerStyle={[
|
||||||
|
isAndroid && {paddingBottom: footerHeight + footerExtensionHeight},
|
||||||
|
contentContainerStyle,
|
||||||
|
]}
|
||||||
automaticallyAdjustsScrollIndicatorInsets={
|
automaticallyAdjustsScrollIndicatorInsets={
|
||||||
automaticallyAdjustsScrollIndicatorInsets
|
automaticallyAdjustsScrollIndicatorInsets
|
||||||
}
|
}
|
||||||
scrollIndicatorInsets={{
|
scrollIndicatorInsets={{
|
||||||
top: headerOffset,
|
top: headerOffset,
|
||||||
right: 1,
|
right: 1,
|
||||||
|
bottom: footerHeight + footerExtensionHeight,
|
||||||
...props.scrollIndicatorInsets,
|
...props.scrollIndicatorInsets,
|
||||||
}}
|
}}
|
||||||
|
contentInset={ios({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: footerHeight + footerExtensionHeight,
|
||||||
|
...props.contentInset,
|
||||||
|
})}
|
||||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||||
contentOffset={contentOffset}
|
contentOffset={contentOffset}
|
||||||
refreshControl={refreshControl}
|
refreshControl={refreshControl}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const safeAreaInsets = useSafeAreaInsets()
|
const safeAreaInsets = useSafeAreaInsets()
|
||||||
const {footerHeight} = useShellLayout()
|
const {setFooterHeight} = useShellLayout()
|
||||||
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} =
|
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} =
|
||||||
useNavigationTabState()
|
useNavigationTabState()
|
||||||
const numUnreadNotifications = useUnreadNotifications()
|
const numUnreadNotifications = useUnreadNotifications()
|
||||||
@@ -152,9 +152,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)},
|
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)},
|
||||||
footerMinimalShellTransform,
|
footerMinimalShellTransform,
|
||||||
]}
|
]}
|
||||||
onLayout={e => {
|
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
|
||||||
footerHeight.set(e.nativeEvent.layout.height)
|
|
||||||
}}>
|
|
||||||
{hasSession ? (
|
{hasSession ? (
|
||||||
<>
|
<>
|
||||||
<Btn
|
<Btn
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function BottomBarWeb() {
|
|||||||
const footerMinimalShellTransform = useMinimalShellFooterTransform()
|
const footerMinimalShellTransform = useMinimalShellFooterTransform()
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const {footerHeight} = useShellLayout()
|
const {setFooterHeight} = useShellLayout()
|
||||||
const hideBorder = useHideBottomBarBorder()
|
const hideBorder = useHideBottomBarBorder()
|
||||||
const iconWidth = 26
|
const iconWidth = 26
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export function BottomBarWeb() {
|
|||||||
: t.atoms.border_contrast_low,
|
: t.atoms.border_contrast_low,
|
||||||
footerMinimalShellTransform,
|
footerMinimalShellTransform,
|
||||||
]}
|
]}
|
||||||
onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}>
|
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
|
||||||
{hasSession ? (
|
{hasSession ? (
|
||||||
<>
|
<>
|
||||||
<NavItem
|
<NavItem
|
||||||
|
|||||||
Reference in New Issue
Block a user