experimental pager sticky header
This commit is contained in:
@@ -19,7 +19,7 @@ export function GrowableAvatar({
|
|||||||
}) {
|
}) {
|
||||||
const pagerContext = usePagerHeaderContext()
|
const pagerContext = usePagerHeaderContext()
|
||||||
|
|
||||||
// pagerContext should only be present on iOS, but better safe than sorry
|
// iOS only
|
||||||
if (!pagerContext || !isIOS) {
|
if (!pagerContext || !isIOS) {
|
||||||
return <View style={style}>{children}</View>
|
return <View style={style}>{children}</View>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,41 +4,55 @@ import {ActivityIndicator} from 'react-native'
|
|||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolation,
|
Extrapolation,
|
||||||
interpolate,
|
interpolate,
|
||||||
|
LayoutAnimationConfig,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
SharedValue,
|
||||||
useAnimatedProps,
|
useAnimatedProps,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {BlurView} from 'expo-blur'
|
import {BlurView} from 'expo-blur'
|
||||||
|
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||||
import {useIsFetching} from '@tanstack/react-query'
|
import {useIsFetching} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
|
import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
|
||||||
import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
|
import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
|
||||||
import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
|
import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
|
||||||
import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
|
import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
|
||||||
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {NavHeader} from './NavHeader'
|
||||||
|
|
||||||
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
|
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
|
||||||
|
|
||||||
export function GrowableBanner({
|
export function GrowableBanner({
|
||||||
backButton,
|
backButton,
|
||||||
|
profile,
|
||||||
|
moderation,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
backButton?: React.ReactNode
|
backButton?: React.ReactNode
|
||||||
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
|
moderation: ModerationDecision
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const pagerContext = usePagerHeaderContext()
|
const pagerContext = usePagerHeaderContext()
|
||||||
|
|
||||||
// pagerContext should only be present on iOS, but better safe than sorry
|
|
||||||
if (!pagerContext || !isIOS) {
|
if (!pagerContext || !isIOS) {
|
||||||
return (
|
return (
|
||||||
<View style={[a.w_full, a.h_full]}>
|
<>
|
||||||
{backButton}
|
<LayoutAnimationConfig skipExiting skipEntering>
|
||||||
{children}
|
<NavHeader
|
||||||
</View>
|
profile={profile}
|
||||||
|
moderation={moderation}
|
||||||
|
backButton={backButton}
|
||||||
|
/>
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
<View style={[a.w_full, a.h_full]}>{children}</View>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +76,7 @@ function GrowableBannerInner({
|
|||||||
}) {
|
}) {
|
||||||
const isFetching = useIsProfileFetching()
|
const isFetching = useIsProfileFetching()
|
||||||
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
|
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
|
||||||
|
const {top: topInset} = useSafeAreaInsets()
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
transform: [
|
transform: [
|
||||||
@@ -103,7 +118,7 @@ function GrowableBannerInner({
|
|||||||
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
|
translateY: interpolate(scrollY.value, [-150, 0], [-150, 0], {
|
||||||
extrapolateRight: Extrapolation.CLAMP,
|
extrapolateRight: Extrapolation.CLAMP,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -127,7 +142,14 @@ function GrowableBannerInner({
|
|||||||
animatedProps={animatedBlurViewProps}
|
animatedProps={animatedBlurViewProps}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{paddingTop: topInset},
|
||||||
|
]}>
|
||||||
<Animated.View style={[animatedSpinnerStyle]}>
|
<Animated.View style={[animatedSpinnerStyle]}>
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
key={animateSpinner ? 'spin' : 'stop'}
|
key={animateSpinner ? 'spin' : 'stop'}
|
||||||
@@ -138,7 +160,8 @@ function GrowableBannerInner({
|
|||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
<Animated.View style={[animatedBackButtonStyle]}>
|
<Animated.View
|
||||||
|
style={[animatedBackButtonStyle, a.absolute, {top: topInset}]}>
|
||||||
{backButton}
|
{backButton}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React, {useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
runOnJS,
|
||||||
|
SlideInUp,
|
||||||
|
SlideOutUp,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {StatusBar} from 'expo-status-bar'
|
||||||
|
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {Shadow} from '#/state/cache/types'
|
||||||
|
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export const PROFILE_NAV_HEADER_HEIGHT = 50
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
|
moderation: ModerationDecision
|
||||||
|
backButton?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavHeader({profile, moderation, backButton}: Props) {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{height: PROFILE_NAV_HEADER_HEIGHT + insets.top + a.border.borderWidth},
|
||||||
|
{paddingTop: insets.top},
|
||||||
|
a.w_full,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.flex_row, a.gap_md, a.px_lg, a.align_center]}>
|
||||||
|
{backButton}
|
||||||
|
<UserAvatar
|
||||||
|
avatar={profile.avatar}
|
||||||
|
size={PROFILE_NAV_HEADER_HEIGHT - 12}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg, a.leading_tight]}>
|
||||||
|
{sanitizeDisplayName(
|
||||||
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_sm, a.leading_tight]}>
|
||||||
|
{isInvalidHandle(profile.handle)
|
||||||
|
? _(msg`⚠Invalid Handle`)
|
||||||
|
: `@${profile.handle}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavHeaderScrollLinked({...props}: Props) {
|
||||||
|
const pagerContext = usePagerHeaderContext()
|
||||||
|
|
||||||
|
if (!pagerContext) {
|
||||||
|
throw new Error(
|
||||||
|
'NavHeaderScrollLinked must be used within a PagerHeaderProvider and cannot be used on web',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {scrollY, headerHeight} = pagerContext
|
||||||
|
const [showHeader, setShowHeader] = useState(false)
|
||||||
|
const {top: topInset} = useSafeAreaInsets()
|
||||||
|
|
||||||
|
const navHeaderHeight = topInset + PROFILE_NAV_HEADER_HEIGHT
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => scrollY.value > 60,
|
||||||
|
(value, prevValue) => {
|
||||||
|
if (value !== prevValue) {
|
||||||
|
runOnJS(setShowHeader)(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [
|
||||||
|
{translateY: Math.min(scrollY.value, headerHeight - navHeaderHeight)},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showHeader ? (
|
||||||
|
<Animated.View
|
||||||
|
entering={SlideInUp.duration(200)}
|
||||||
|
exiting={SlideOutUp.duration(200)}
|
||||||
|
style={[
|
||||||
|
animatedStyle,
|
||||||
|
a.absolute,
|
||||||
|
{top: 0, left: 0, right: 0},
|
||||||
|
a.z_50,
|
||||||
|
]}>
|
||||||
|
<NavHeader {...props} />
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<StatusBar style="light" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,11 +16,12 @@ import {useSession} from '#/state/session'
|
|||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||||
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
||||||
import {GrowableAvatar} from './GrowableAvatar'
|
import {GrowableAvatar} from './GrowableAvatar'
|
||||||
import {GrowableBanner} from './GrowableBanner'
|
import {GrowableBanner} from './GrowableBanner'
|
||||||
|
import {NavHeaderScrollLinked} from './NavHeader'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
@@ -69,6 +70,8 @@ let ProfileHeaderShell = ({
|
|||||||
pointerEvents={isIOS ? 'auto' : 'none'}
|
pointerEvents={isIOS ? 'auto' : 'none'}
|
||||||
style={[a.relative, {height: 150}]}>
|
style={[a.relative, {height: 150}]}>
|
||||||
<GrowableBanner
|
<GrowableBanner
|
||||||
|
profile={profile}
|
||||||
|
moderation={moderation}
|
||||||
backButton={
|
backButton={
|
||||||
<>
|
<>
|
||||||
{!isDesktop && !hideBackButton && (
|
{!isDesktop && !hideBackButton && (
|
||||||
@@ -105,9 +108,7 @@ let ProfileHeaderShell = ({
|
|||||||
)}
|
)}
|
||||||
</GrowableBanner>
|
</GrowableBanner>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{!isPlaceholderProfile && (
|
{!isPlaceholderProfile && (
|
||||||
<View
|
<View
|
||||||
style={[a.px_lg, a.py_xs]}
|
style={[a.px_lg, a.py_xs]}
|
||||||
@@ -119,7 +120,6 @@ let ProfileHeaderShell = ({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GrowableAvatar style={styles.aviPosition}>
|
<GrowableAvatar style={styles.aviPosition}>
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
testID="profileHeaderAviButton"
|
testID="profileHeaderAviButton"
|
||||||
@@ -143,6 +143,9 @@ let ProfileHeaderShell = ({
|
|||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</GrowableAvatar>
|
</GrowableAvatar>
|
||||||
|
{native(
|
||||||
|
<NavHeaderScrollLinked profile={profile} moderation={moderation} />,
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,51 @@
|
|||||||
import React, {useContext} from 'react'
|
import React, {useContext, useMemo} from 'react'
|
||||||
import {SharedValue} from 'react-native-reanimated'
|
import {SharedValue} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
|
|
||||||
export const PagerHeaderContext =
|
export const PagerHeaderContext = React.createContext<{
|
||||||
React.createContext<SharedValue<number> | null>(null)
|
scrollY: SharedValue<number>
|
||||||
|
headerHeight: number
|
||||||
|
tabBarHeight: number
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes the scrollY value to the pager header's banner, so it can grow on
|
* Passes the scrollY value to the pager header's banner, so it can grow on
|
||||||
* overscroll on iOS. Not necessary to use this context provider on other platforms.
|
* overscroll on iOS, and handle the status bar. Not necessary to use this context provider on web.
|
||||||
*
|
*
|
||||||
* @platform ios
|
* @platform ios, android
|
||||||
*/
|
*/
|
||||||
export function PagerHeaderProvider({
|
export function PagerHeaderProvider({
|
||||||
scrollY,
|
scrollY,
|
||||||
children,
|
children,
|
||||||
|
headerHeight,
|
||||||
|
tabBarHeight,
|
||||||
}: {
|
}: {
|
||||||
scrollY: SharedValue<number>
|
scrollY: SharedValue<number>
|
||||||
|
headerHeight: number
|
||||||
|
tabBarHeight: number
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({scrollY, headerHeight, tabBarHeight}),
|
||||||
|
[scrollY, headerHeight, tabBarHeight],
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<PagerHeaderContext.Provider value={scrollY}>
|
<PagerHeaderContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</PagerHeaderContext.Provider>
|
</PagerHeaderContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePagerHeaderContext() {
|
export function usePagerHeaderContext() {
|
||||||
const scrollY = useContext(PagerHeaderContext)
|
const context = useContext(PagerHeaderContext)
|
||||||
if (isIOS) {
|
if (isNative) {
|
||||||
if (!scrollY) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'usePagerHeaderContext must be used within a HeaderProvider',
|
'usePagerHeaderContext must be used within a HeaderProvider',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return {scrollY}
|
return context
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface PagerWithHeaderProps {
|
|||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
allowHeaderOverScroll?: boolean
|
allowHeaderOverScroll?: boolean
|
||||||
|
headerMinHeight?: number
|
||||||
}
|
}
|
||||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
function PageWithHeaderImpl(
|
function PageWithHeaderImpl(
|
||||||
@@ -56,6 +57,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
onPageSelected,
|
onPageSelected,
|
||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
allowHeaderOverScroll,
|
allowHeaderOverScroll,
|
||||||
|
headerMinHeight,
|
||||||
}: PagerWithHeaderProps,
|
}: PagerWithHeaderProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -83,7 +85,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
const renderTabBar = React.useCallback(
|
const renderTabBar = React.useCallback(
|
||||||
(props: RenderTabBarFnProps) => {
|
(props: RenderTabBarFnProps) => {
|
||||||
return (
|
return (
|
||||||
<PagerHeaderProvider scrollY={scrollY}>
|
<PagerHeaderProvider
|
||||||
|
scrollY={scrollY}
|
||||||
|
headerHeight={headerOnlyHeight}
|
||||||
|
tabBarHeight={tabBarHeight}>
|
||||||
<PagerTabBar
|
<PagerTabBar
|
||||||
headerOnlyHeight={headerOnlyHeight}
|
headerOnlyHeight={headerOnlyHeight}
|
||||||
items={items}
|
items={items}
|
||||||
@@ -97,6 +102,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
scrollY={scrollY}
|
scrollY={scrollY}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||||
|
headerMinHeight={headerMinHeight}
|
||||||
/>
|
/>
|
||||||
</PagerHeaderProvider>
|
</PagerHeaderProvider>
|
||||||
)
|
)
|
||||||
@@ -113,6 +119,8 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
scrollY,
|
scrollY,
|
||||||
testID,
|
testID,
|
||||||
allowHeaderOverScroll,
|
allowHeaderOverScroll,
|
||||||
|
headerMinHeight,
|
||||||
|
tabBarHeight,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,6 +239,7 @@ let PagerTabBar = ({
|
|||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
allowHeaderOverScroll,
|
allowHeaderOverScroll,
|
||||||
|
headerMinHeight = 0,
|
||||||
}: {
|
}: {
|
||||||
currentPage: number
|
currentPage: number
|
||||||
headerOnlyHeight: number
|
headerOnlyHeight: number
|
||||||
@@ -244,9 +253,11 @@ let PagerTabBar = ({
|
|||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
onSelect?: (index: number) => void
|
onSelect?: (index: number) => void
|
||||||
allowHeaderOverScroll?: boolean
|
allowHeaderOverScroll?: boolean
|
||||||
|
headerMinHeight?: number
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const headerTransform = useAnimatedStyle(() => {
|
const headerTransform = useAnimatedStyle(() => {
|
||||||
const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1
|
const translateY =
|
||||||
|
Math.min(scrollY.value, headerOnlyHeight - headerMinHeight) * -1
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, {useCallback, useMemo} from 'react'
|
import React, {useCallback, useMemo} from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
AppBskyGraphGetActorStarterPacks,
|
AppBskyGraphGetActorStarterPacks,
|
||||||
@@ -42,6 +43,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
|
|||||||
import {ListRef} from '#/view/com/util/List'
|
import {ListRef} from '#/view/com/util/List'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
||||||
|
import {PROFILE_NAV_HEADER_HEIGHT} from '#/screens/Profile/Header/NavHeader'
|
||||||
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||||
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
||||||
import {web} from '#/alf'
|
import {web} from '#/alf'
|
||||||
@@ -58,7 +60,7 @@ interface SectionRef {
|
|||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||||
export function ProfileScreen(props: Props) {
|
export function ProfileScreen(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="profileScreen">
|
<Layout.Screen testID="profileScreen" disableTopPadding>
|
||||||
<ProfileScreenInner {...props} />
|
<ProfileScreenInner {...props} />
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
@@ -186,6 +188,7 @@ function ProfileScreenLoaded({
|
|||||||
const [currentPage, setCurrentPage] = React.useState(0)
|
const [currentPage, setCurrentPage] = React.useState(0)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
|
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
|
||||||
|
|
||||||
@@ -369,7 +372,8 @@ function ProfileScreenLoaded({
|
|||||||
onPageSelected={onPageSelected}
|
onPageSelected={onPageSelected}
|
||||||
onCurrentPageSelected={onCurrentPageSelected}
|
onCurrentPageSelected={onCurrentPageSelected}
|
||||||
renderHeader={renderHeader}
|
renderHeader={renderHeader}
|
||||||
allowHeaderOverScroll>
|
allowHeaderOverScroll
|
||||||
|
headerMinHeight={insets.top + PROFILE_NAV_HEADER_HEIGHT}>
|
||||||
{showFiltersTab
|
{showFiltersTab
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileLabelsSection
|
<ProfileLabelsSection
|
||||||
|
|||||||
Reference in New Issue
Block a user