Skip to content

Commit

Permalink
[Layout] Bleed profile banner into safe area (#6967)
Browse files Browse the repository at this point in the history
* bleed profile banner into safe area

(cherry picked from commit 50b3a4d)

* pointer events none when hidden

(cherry picked from commit bae2c7b)

* fix web

(cherry picked from commit e3f9597)

* add status bar shadow

* rm log

* rm mini header

* speed up animation

* pass bool rather than int in light status bar
  • Loading branch information
mozzius authored Dec 12, 2024
1 parent 4b32b0a commit ffc63dc
Show file tree
Hide file tree
Showing 14 changed files with 322 additions and 47 deletions.
5 changes: 4 additions & 1 deletion src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
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 ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
Expand Down Expand Up @@ -209,7 +210,9 @@ function App() {
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
Expand Down
5 changes: 4 additions & 1 deletion src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
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 ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
Expand Down Expand Up @@ -181,7 +182,9 @@ function App() {
<PortalProvider>
<StarterPackProvider>
<IntentDialogProvider>
<InnerApp />
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Layout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export function TitleText({
gtMobile && a.text_xl,
style,
]}
numberOfLines={2}>
numberOfLines={2}
emoji>
{children}
</Text>
)
Expand Down
15 changes: 12 additions & 3 deletions src/screens/Profile/Header/GrowableBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Animated, {
useAnimatedReaction,
useAnimatedStyle,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {BlurView} from 'expo-blur'
import {useIsFetching} from '@tanstack/react-query'

Expand All @@ -32,7 +33,7 @@ export function GrowableBanner({
}) {
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) {
return (
<View style={[a.w_full, a.h_full]}>
Expand Down Expand Up @@ -60,6 +61,7 @@ function GrowableBannerInner({
backButton?: React.ReactNode
children: React.ReactNode
}) {
const {top: topInset} = useSafeAreaInsets()
const isFetching = useIsProfileFetching()
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})

Expand Down Expand Up @@ -104,7 +106,7 @@ function GrowableBannerInner({
const animatedBackButtonStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
Expand All @@ -128,7 +130,14 @@ function GrowableBannerInner({
animatedProps={animatedBlurViewProps}
/>
</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]}>
<ActivityIndicator
key={animateSpinner ? 'spin' : 'stop'}
Expand Down
31 changes: 19 additions & 12 deletions src/screens/Profile/Header/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, {memo} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'

import {BACK_HITSLOP} from '#/lib/constants'
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {isIOS} from '#/platform/detection'
import {Shadow} from '#/state/cache/types'
Expand All @@ -18,11 +17,13 @@ import {useSession} from '#/state/session'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {UserAvatar} from '#/view/com/util/UserAvatar'
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 {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
import {GrowableAvatar} from './GrowableAvatar'
import {GrowableBanner} from './GrowableBanner'
import {StatusBarShadow} from './StatusBarShadow'

interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
Expand All @@ -43,7 +44,8 @@ let ProfileHeaderShell = ({
const {_} = useLingui()
const {openLightbox} = useLightboxControls()
const navigation = useNavigation<NavigationProp>()
const {isDesktop} = useWebMediaQueries()
const {top: topInset} = useSafeAreaInsets()

const aviRef = useHandleRef()

const onPressBack = React.useCallback(() => {
Expand Down Expand Up @@ -100,23 +102,29 @@ let ProfileHeaderShell = ({
<View
pointerEvents={isIOS ? 'auto' : 'box-none'}
style={[a.relative, {height: 150}]}>
<StatusBarShadow />
<GrowableBanner
backButton={
<>
{!isDesktop && !hideBackButton && (
{!hideBackButton && (
<TouchableWithoutFeedback
testID="profileHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<View style={styles.backBtnWrapper}>
<FontAwesomeIcon
size={18}
icon="angle-left"
color="white"
/>
<View
style={[
styles.backBtnWrapper,
{
top: platform({
web: 10,
default: topInset,
}),
},
]}>
<ArrowLeftIcon size="lg" fill="white" />
</View>
</TouchableWithoutFeedback>
)}
Expand Down Expand Up @@ -186,7 +194,6 @@ export {ProfileHeaderShell}
const styles = StyleSheet.create({
backBtnWrapper: {
position: 'absolute',
top: 10,
left: 10,
width: 30,
height: 30,
Expand Down
56 changes: 56 additions & 0 deletions src/screens/Profile/Header/StatusBarShadow.tsx
Original file line number Diff line number Diff line change
@@ -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},
]}
/>
)
}
3 changes: 3 additions & 0 deletions src/screens/Profile/Header/StatusBarShadow.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function StatusBarShadow() {
return null
}
119 changes: 112 additions & 7 deletions src/screens/Profile/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import React, {memo} from 'react'
import {StyleSheet, View} from 'react-native'
import React, {memo, useState} from 'react'
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 {
AppBskyActorDefs,
AppBskyLabelerDefs,
ModerationOpts,
RichText as RichTextAPI,
} 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 {useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
import {ProfileHeaderStandard} from './ProfileHeaderStandard'

Expand Down Expand Up @@ -43,20 +54,114 @@ interface Props {
moderationOpts: ModerationOpts
hideBackButton?: 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.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)
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({
avi: {
position: 'absolute',
Expand Down
Loading

0 comments on commit ffc63dc

Please sign in to comment.