[Layout] Bleed profile banner into safe area (#6967)

* bleed profile banner into safe area

(cherry picked from commit 50b3a4d0c6fd94b583ffe4efa65de35c81ae7f4e)

* pointer events none when hidden

(cherry picked from commit bae2c7b2dd6d7f858a98812196628308c0877755)

* fix web

(cherry picked from commit e3f9597170375f2903b6e567b963f008ec95aed1)

* add status bar shadow

* rm log

* rm mini header

* speed up animation

* pass bool rather than int in light status bar
This commit is contained in:
Samuel Newman
2024-12-12 17:46:19 +00:00
committed by GitHub
parent 4b32b0a71b
commit ffc63dc85f
14 changed files with 322 additions and 47 deletions
+4 -1
View File
@@ -54,6 +54,7 @@ import {
import {readLastActiveAccount} from '#/state/session/util' import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer' import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
@@ -209,7 +210,9 @@ function App() {
<SafeAreaProvider <SafeAreaProvider
initialMetrics={initialWindowMetrics}> initialMetrics={initialWindowMetrics}>
<IntentDialogProvider> <IntentDialogProvider>
<InnerApp /> <LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider> </IntentDialogProvider>
</SafeAreaProvider> </SafeAreaProvider>
</StarterPackProvider> </StarterPackProvider>
+4 -1
View File
@@ -40,6 +40,7 @@ import {
import {readLastActiveAccount} from '#/state/session/util' import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer' import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
@@ -181,7 +182,9 @@ function App() {
<PortalProvider> <PortalProvider>
<StarterPackProvider> <StarterPackProvider>
<IntentDialogProvider> <IntentDialogProvider>
<InnerApp /> <LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider> </IntentDialogProvider>
</StarterPackProvider> </StarterPackProvider>
</PortalProvider> </PortalProvider>
+2 -1
View File
@@ -175,7 +175,8 @@ export function TitleText({
gtMobile && a.text_xl, gtMobile && a.text_xl,
style, style,
]} ]}
numberOfLines={2}> numberOfLines={2}
emoji>
{children} {children}
</Text> </Text>
) )
+12 -3
View File
@@ -10,6 +10,7 @@ import Animated, {
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 {useIsFetching} from '@tanstack/react-query' import {useIsFetching} from '@tanstack/react-query'
@@ -32,7 +33,7 @@ export function GrowableBanner({
}) { }) {
const pagerContext = usePagerHeaderContext() const pagerContext = usePagerHeaderContext()
// pagerContext should only be present on iOS, but better safe than sorry // plain non-growable mode for Android/Web
if (!pagerContext || !isIOS) { if (!pagerContext || !isIOS) {
return ( return (
<View style={[a.w_full, a.h_full]}> <View style={[a.w_full, a.h_full]}>
@@ -60,6 +61,7 @@ function GrowableBannerInner({
backButton?: React.ReactNode backButton?: React.ReactNode
children: React.ReactNode children: React.ReactNode
}) { }) {
const {top: topInset} = useSafeAreaInsets()
const isFetching = useIsProfileFetching() const isFetching = useIsProfileFetching()
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY}) const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
@@ -104,7 +106,7 @@ function GrowableBannerInner({
const animatedBackButtonStyle = useAnimatedStyle(() => ({ const animatedBackButtonStyle = useAnimatedStyle(() => ({
transform: [ transform: [
{ {
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], { translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], {
extrapolateRight: Extrapolation.CLAMP, extrapolateRight: Extrapolation.CLAMP,
}), }),
}, },
@@ -128,7 +130,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,
{top: topInset - (isIOS ? 15 : 0)},
a.justify_center,
a.align_center,
]}>
<Animated.View style={[animatedSpinnerStyle]}> <Animated.View style={[animatedSpinnerStyle]}>
<ActivityIndicator <ActivityIndicator
key={animateSpinner ? 'spin' : 'stop'} key={animateSpinner ? 'spin' : 'stop'}
+19 -12
View File
@@ -1,15 +1,14 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {BACK_HITSLOP} from '#/lib/constants' import {BACK_HITSLOP} from '#/lib/constants'
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
@@ -18,11 +17,13 @@ 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, platform, useTheme} from '#/alf'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
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 {StatusBarShadow} from './StatusBarShadow'
interface Props { interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
@@ -43,7 +44,8 @@ let ProfileHeaderShell = ({
const {_} = useLingui() const {_} = useLingui()
const {openLightbox} = useLightboxControls() const {openLightbox} = useLightboxControls()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isDesktop} = useWebMediaQueries() const {top: topInset} = useSafeAreaInsets()
const aviRef = useHandleRef() const aviRef = useHandleRef()
const onPressBack = React.useCallback(() => { const onPressBack = React.useCallback(() => {
@@ -100,10 +102,11 @@ let ProfileHeaderShell = ({
<View <View
pointerEvents={isIOS ? 'auto' : 'box-none'} pointerEvents={isIOS ? 'auto' : 'box-none'}
style={[a.relative, {height: 150}]}> style={[a.relative, {height: 150}]}>
<StatusBarShadow />
<GrowableBanner <GrowableBanner
backButton={ backButton={
<> <>
{!isDesktop && !hideBackButton && ( {!hideBackButton && (
<TouchableWithoutFeedback <TouchableWithoutFeedback
testID="profileHeaderBackBtn" testID="profileHeaderBackBtn"
onPress={onPressBack} onPress={onPressBack}
@@ -111,12 +114,17 @@ let ProfileHeaderShell = ({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Back`)} accessibilityLabel={_(msg`Back`)}
accessibilityHint=""> accessibilityHint="">
<View style={styles.backBtnWrapper}> <View
<FontAwesomeIcon style={[
size={18} styles.backBtnWrapper,
icon="angle-left" {
color="white" top: platform({
/> web: 10,
default: topInset,
}),
},
]}>
<ArrowLeftIcon size="lg" fill="white" />
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
)} )}
@@ -186,7 +194,6 @@ export {ProfileHeaderShell}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
backBtnWrapper: { backBtnWrapper: {
position: 'absolute', position: 'absolute',
top: 10,
left: 10, left: 10,
width: 30, width: 30,
height: 30, height: 30,
@@ -0,0 +1,56 @@
import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {isIOS} from '#/platform/detection'
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
import {atoms as a} from '#/alf'
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
export function StatusBarShadow() {
const {top: topInset} = useSafeAreaInsets()
const pagerContext = usePagerHeaderContext()
if (isIOS && pagerContext) {
const {scrollY} = pagerContext
return <StatusBarShadowInnner scrollY={scrollY} />
}
return (
<LinearGradient
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
style={[
a.absolute,
a.z_10,
{height: topInset, top: 0, left: 0, right: 0},
]}
/>
)
}
function StatusBarShadowInnner({scrollY}: {scrollY: SharedValue<number>}) {
const {top: topInset} = useSafeAreaInsets()
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: Math.min(0, scrollY.get()),
},
],
}
})
return (
<AnimatedLinearGradient
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
style={[
animatedStyle,
a.absolute,
a.z_10,
{height: topInset, top: 0, left: 0, right: 0},
]}
/>
)
}
@@ -0,0 +1,3 @@
export function StatusBarShadow() {
return null
}
+112 -7
View File
@@ -1,14 +1,25 @@
import React, {memo} from 'react' import React, {memo, useState} from 'react'
import {StyleSheet, View} from 'react-native' import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
import Animated, {
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
AppBskyLabelerDefs, AppBskyLabelerDefs,
ModerationOpts, ModerationOpts,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {useIsFocused} from '@react-navigation/native'
import {isNative} from '#/platform/detection'
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler' import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
import {ProfileHeaderStandard} from './ProfileHeaderStandard' import {ProfileHeaderStandard} from './ProfileHeaderStandard'
@@ -43,20 +54,114 @@ interface Props {
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
hideBackButton?: boolean hideBackButton?: boolean
isPlaceholderProfile?: boolean isPlaceholderProfile?: boolean
setMinimumHeight: (height: number) => void
} }
let ProfileHeader = (props: Props): React.ReactNode => { let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
let content
if (props.profile.associated?.labeler) { if (props.profile.associated?.labeler) {
if (!props.labeler) { if (!props.labeler) {
return <ProfileHeaderLoading /> content = <ProfileHeaderLoading />
} else {
content = <ProfileHeaderLabeler {...props} labeler={props.labeler} />
} }
return <ProfileHeaderLabeler {...props} labeler={props.labeler} /> } else {
content = <ProfileHeaderStandard {...props} />
} }
return <ProfileHeaderStandard {...props} />
return (
<>
{isNative && (
<MinimalHeader
onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
profile={props.profile}
hideBackButton={props.hideBackButton}
/>
)}
{content}
</>
)
} }
ProfileHeader = memo(ProfileHeader) ProfileHeader = memo(ProfileHeader)
export {ProfileHeader} export {ProfileHeader}
const MinimalHeader = React.memo(function MinimalHeader({
onLayout,
}: {
onLayout: (e: LayoutChangeEvent) => void
profile: AppBskyActorDefs.ProfileViewDetailed
hideBackButton?: boolean
}) {
const t = useTheme()
const insets = useSafeAreaInsets()
const ctx = usePagerHeaderContext()
const [visible, setVisible] = useState(false)
const [minimalHeaderHeight, setMinimalHeaderHeight] = React.useState(0)
const isScreenFocused = useIsFocused()
if (!ctx) throw new Error('MinimalHeader cannot be used on web')
const {scrollY, headerHeight} = ctx
const animatedStyle = useAnimatedStyle(() => {
// if we don't yet have the min header height in JS, hide
if (!_WORKLET || minimalHeaderHeight === 0) {
return {
opacity: 0,
}
}
const pastThreshold = scrollY.get() > 100
return {
opacity: pastThreshold
? withTiming(1, {duration: 75})
: withTiming(0, {duration: 75}),
transform: [
{
translateY: Math.min(
scrollY.get(),
headerHeight - minimalHeaderHeight,
),
},
],
}
})
useAnimatedReaction(
() => scrollY.get() > 100,
(value, prev) => {
if (prev !== value) {
runOnJS(setVisible)(value)
}
},
)
useSetLightStatusBar(isScreenFocused && !visible)
return (
<Animated.View
pointerEvents={visible ? 'auto' : 'none'}
aria-hidden={!visible}
accessibilityElementsHidden={!visible}
importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}
onLayout={evt => {
setMinimalHeaderHeight(evt.nativeEvent.layout.height)
onLayout(evt)
}}
style={[
a.absolute,
a.z_50,
t.atoms.bg,
{
top: 0,
left: 0,
right: 0,
paddingTop: insets.top,
},
animatedStyle,
]}
/>
)
})
MinimalHeader.displayName = 'MinimalHeader'
const styles = StyleSheet.create({ const styles = StyleSheet.create({
avi: { avi: {
position: 'absolute', position: 'absolute',
+45
View File
@@ -0,0 +1,45 @@
import {createContext, useContext, useEffect, useState} from 'react'
import {isWeb} from '#/platform/detection'
import {IS_DEV} from '#/env'
const LightStatusBarRefCountContext = createContext<boolean>(false)
const SetLightStatusBarRefCountContext = createContext<React.Dispatch<
React.SetStateAction<number>
> | null>(null)
export function useLightStatusBar() {
return useContext(LightStatusBarRefCountContext)
}
export function useSetLightStatusBar(enabled: boolean) {
const setRefCount = useContext(SetLightStatusBarRefCountContext)
useEffect(() => {
// noop on web -sfn
if (isWeb) return
if (!setRefCount) {
if (IS_DEV)
console.error(
'useLightStatusBar was used without a SetLightStatusBarRefCountContext provider',
)
return
}
if (enabled) {
setRefCount(prev => prev + 1)
return () => setRefCount(prev => prev - 1)
}
}, [enabled, setRefCount])
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [refCount, setRefCount] = useState(0)
return (
<SetLightStatusBarRefCountContext.Provider value={setRefCount}>
<LightStatusBarRefCountContext.Provider value={refCount > 0}>
{children}
</LightStatusBarRefCountContext.Provider>
</SetLightStatusBarRefCountContext.Provider>
)
}
+19 -11
View File
@@ -1,40 +1,48 @@
import React, {useContext} from 'react' import React, {useContext} 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
} | null>(null)
/** /**
* Passes the scrollY value to the pager header's banner, so it can grow on * Passes information about the scroll position and header height down via
* overscroll on iOS. Not necessary to use this context provider on other platforms. * context for the pager header to consume.
* *
* @platform ios * @platform ios, android
*/ */
export function PagerHeaderProvider({ export function PagerHeaderProvider({
scrollY, scrollY,
headerHeight,
children, children,
}: { }: {
scrollY: SharedValue<number> scrollY: SharedValue<number>
headerHeight: number
children: React.ReactNode children: React.ReactNode
}) { }) {
const value = React.useMemo(
() => ({scrollY, headerHeight}),
[scrollY, headerHeight],
)
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 ctx = useContext(PagerHeaderContext)
if (isIOS) { if (isNative) {
if (!scrollY) { if (!ctx) {
throw new Error( throw new Error(
'usePagerHeaderContext must be used within a HeaderProvider', 'usePagerHeaderContext must be used within a HeaderProvider',
) )
} }
return {scrollY} return ctx
} else { } else {
return null return null
} }
+20 -5
View File
@@ -38,7 +38,11 @@ export interface PagerWithHeaderProps {
| ((props: PagerWithHeaderChildParams) => JSX.Element) | ((props: PagerWithHeaderChildParams) => JSX.Element)
items: string[] items: string[]
isHeaderReady: boolean isHeaderReady: boolean
renderHeader?: () => JSX.Element renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: (height: number) => void
}) => JSX.Element
initialPage?: number initialPage?: number
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
@@ -83,7 +87,9 @@ 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}>
<PagerTabBar <PagerTabBar
headerOnlyHeight={headerOnlyHeight} headerOnlyHeight={headerOnlyHeight}
items={items} items={items}
@@ -237,7 +243,11 @@ let PagerTabBar = ({
items: string[] items: string[]
testID?: string testID?: string
scrollY: SharedValue<number> scrollY: SharedValue<number>
renderHeader?: () => JSX.Element renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: (height: number) => void
}) => JSX.Element
onHeaderOnlyLayout: (height: number) => void onHeaderOnlyLayout: (height: number) => void
onTabBarLayout: (e: LayoutChangeEvent) => void onTabBarLayout: (e: LayoutChangeEvent) => void
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
@@ -246,8 +256,13 @@ let PagerTabBar = ({
dragProgress: SharedValue<number> dragProgress: SharedValue<number>
dragState: SharedValue<'idle' | 'dragging' | 'settling'> dragState: SharedValue<'idle' | 'dragging' | 'settling'>
}): React.ReactNode => { }): React.ReactNode => {
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
const headerTransform = useAnimatedStyle(() => { const headerTransform = useAnimatedStyle(() => {
const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1 const translateY =
Math.min(
scrollY.get(),
Math.max(headerOnlyHeight - minimumHeaderHeight, 0),
) * -1
return { return {
transform: [ transform: [
{ {
@@ -267,7 +282,7 @@ let PagerTabBar = ({
ref={headerRef} ref={headerRef}
pointerEvents={isIOS ? 'auto' : 'box-none'} pointerEvents={isIOS ? 'auto' : 'box-none'}
collapsable={false}> collapsable={false}>
{renderHeader?.()} {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})}
{ {
// It wouldn't be enough to place `onLayout` on the parent node because // It wouldn't be enough to place `onLayout` on the parent node because
// this would risk measuring before `isHeaderReady` has turned `true`. // this would risk measuring before `isHeaderReady` has turned `true`.
+13 -3
View File
@@ -21,7 +21,11 @@ export interface PagerWithHeaderProps {
| ((props: PagerWithHeaderChildParams) => JSX.Element) | ((props: PagerWithHeaderChildParams) => JSX.Element)
items: string[] items: string[]
isHeaderReady: boolean isHeaderReady: boolean
renderHeader?: () => JSX.Element renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: () => void
}) => JSX.Element
initialPage?: number initialPage?: number
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
@@ -115,7 +119,11 @@ let PagerTabBar = ({
currentPage: number currentPage: number
items: string[] items: string[]
testID?: string testID?: string
renderHeader?: () => JSX.Element renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: () => void
}) => JSX.Element
isHeaderReady: boolean isHeaderReady: boolean
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void onSelect?: (index: number) => void
@@ -123,7 +131,7 @@ let PagerTabBar = ({
}): React.ReactNode => { }): React.ReactNode => {
return ( return (
<> <>
<Layout.Center>{renderHeader?.()}</Layout.Center> <Layout.Center>{renderHeader?.({setMinimumHeight: noop})}</Layout.Center>
{tabBarAnchor} {tabBarAnchor}
<Layout.Center <Layout.Center
style={web([ style={web([
@@ -175,3 +183,5 @@ function toArray<T>(v: T | T[]): T[] {
} }
return [v] return [v]
} }
function noop() {}
+8 -2
View File
@@ -43,6 +43,7 @@ import {ListRef} from '#/view/com/util/List'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
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 {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {ScreenHider} from '#/components/moderation/ScreenHider' import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
@@ -56,7 +57,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" style={[a.pt_0]}>
<ProfileScreenInner {...props} /> <ProfileScreenInner {...props} />
</Layout.Screen> </Layout.Screen>
) )
@@ -329,7 +330,11 @@ function ProfileScreenLoaded({
// rendering // rendering
// = // =
const renderHeader = () => { const renderHeader = ({
setMinimumHeight,
}: {
setMinimumHeight: (height: number) => void
}) => {
return ( return (
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}> <ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
<ProfileHeader <ProfileHeader
@@ -339,6 +344,7 @@ function ProfileScreenLoaded({
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
hideBackButton={hideBackButton} hideBackButton={hideBackButton}
isPlaceholderProfile={showPlaceholder} isPlaceholderProfile={showPlaceholder}
setMinimumHeight={setMinimumHeight}
/> />
</ExpoScrollForwarderView> </ExpoScrollForwarderView>
) )
+5 -1
View File
@@ -18,6 +18,7 @@ import {
useIsDrawerSwipeDisabled, useIsDrawerSwipeDisabled,
useSetDrawerOpen, useSetDrawerOpen,
} from '#/state/shell' } from '#/state/shell'
import {useLightStatusBar} from '#/state/shell/light-status-bar'
import {useCloseAnyActiveElement} from '#/state/util' import {useCloseAnyActiveElement} from '#/state/util'
import {Lightbox} from '#/view/com/lightbox/Lightbox' import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal' import {ModalsContainer} from '#/view/com/modals/Modal'
@@ -154,6 +155,7 @@ function ShellInner() {
export const Shell: React.FC = function ShellImpl() { export const Shell: React.FC = function ShellImpl() {
const {fullyExpandedCount} = useDialogStateControlContext() const {fullyExpandedCount} = useDialogStateControlContext()
const lightStatusBar = useLightStatusBar()
const t = useTheme() const t = useTheme()
useIntentHandler() useIntentHandler()
@@ -165,7 +167,9 @@ export const Shell: React.FC = function ShellImpl() {
<View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}> <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}>
<StatusBar <StatusBar
style={ style={
t.name !== 'light' || (isIOS && fullyExpandedCount > 0) t.name !== 'light' ||
(isIOS && fullyExpandedCount > 0) ||
lightStatusBar
? 'light' ? 'light'
: 'dark' : 'dark'
} }