Header blurred banner on overscroll (take 2) (#5474)
* grow banner when overscrolling * add blurview * make backdrop blur as it scrolls * add activity indicator * use rotated spinner instead of arrow * persist position of back button * make back button prettier * make blur less jarring * Unify effects * Tweak impl * determine if should animate based on scroll amount * sign comment --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
@@ -120,6 +120,7 @@
|
|||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^51.0.8",
|
"expo": "^51.0.8",
|
||||||
"expo-application": "^5.9.1",
|
"expo-application": "^5.9.1",
|
||||||
|
"expo-blur": "^13.0.2",
|
||||||
"expo-build-properties": "^0.12.1",
|
"expo-build-properties": "^0.12.1",
|
||||||
"expo-camera": "~15.0.9",
|
"expo-camera": "~15.0.9",
|
||||||
"expo-clipboard": "^6.0.3",
|
"expo-clipboard": "^6.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {ActivityIndicator} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
Extrapolation,
|
||||||
|
interpolate,
|
||||||
|
runOnJS,
|
||||||
|
SharedValue,
|
||||||
|
useAnimatedProps,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {BlurView} from 'expo-blur'
|
||||||
|
import {useIsFetching} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
|
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 FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
|
||||||
|
import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
|
||||||
|
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
|
||||||
|
|
||||||
|
export function GrowableBanner({
|
||||||
|
backButton,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
backButton?: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const pagerContext = usePagerHeaderContext()
|
||||||
|
|
||||||
|
// pagerContext should only be present on iOS, but better safe than sorry
|
||||||
|
if (!pagerContext || !isIOS) {
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full, a.h_full]}>
|
||||||
|
{backButton}
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {scrollY} = pagerContext
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrowableBannerInner scrollY={scrollY} backButton={backButton}>
|
||||||
|
{children}
|
||||||
|
</GrowableBannerInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrowableBannerInner({
|
||||||
|
scrollY,
|
||||||
|
backButton,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
scrollY: SharedValue<number>
|
||||||
|
backButton?: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const isFetching = useIsProfileFetching()
|
||||||
|
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: interpolate(scrollY.value, [-150, 0], [2, 1], {
|
||||||
|
extrapolateRight: Extrapolation.CLAMP,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const animatedBlurViewProps = useAnimatedProps(() => {
|
||||||
|
return {
|
||||||
|
intensity: interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[-400, -100, -15],
|
||||||
|
[70, 60, 0],
|
||||||
|
Extrapolation.CLAMP,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const animatedSpinnerStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
display: scrollY.value < 0 ? 'flex' : 'none',
|
||||||
|
opacity: interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[-60, -15],
|
||||||
|
[1, 0],
|
||||||
|
Extrapolation.CLAMP,
|
||||||
|
),
|
||||||
|
transform: [
|
||||||
|
{translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])},
|
||||||
|
{rotate: '90deg'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
|
||||||
|
extrapolateRight: Extrapolation.CLAMP,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{left: 0, right: 0, bottom: 0},
|
||||||
|
{height: 150},
|
||||||
|
{transformOrigin: 'bottom'},
|
||||||
|
animatedStyle,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
<AnimatedBlurView
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
tint="dark"
|
||||||
|
animatedProps={animatedBlurViewProps}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||||
|
<Animated.View style={[animatedSpinnerStyle]}>
|
||||||
|
<ActivityIndicator
|
||||||
|
key={animateSpinner ? 'spin' : 'stop'}
|
||||||
|
size="large"
|
||||||
|
color="white"
|
||||||
|
animating={animateSpinner}
|
||||||
|
hidesWhenStopped={false}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
<Animated.View style={[animatedBackButtonStyle]}>
|
||||||
|
{backButton}
|
||||||
|
</Animated.View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useIsProfileFetching() {
|
||||||
|
// are any of the profile-related queries fetching?
|
||||||
|
return [
|
||||||
|
useIsFetching({queryKey: [FEED_RQKEY_ROOT]}),
|
||||||
|
useIsFetching({queryKey: [FEEDGEN_RQKEY_ROOT]}),
|
||||||
|
useIsFetching({queryKey: [LIST_RQKEY_ROOT]}),
|
||||||
|
useIsFetching({queryKey: [STARTERPACK_RQKEY_ROOT]}),
|
||||||
|
].some(isFetching => isFetching)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useShouldAnimateSpinner({
|
||||||
|
isFetching,
|
||||||
|
scrollY,
|
||||||
|
}: {
|
||||||
|
isFetching: boolean
|
||||||
|
scrollY: SharedValue<number>
|
||||||
|
}) {
|
||||||
|
const [isOverscrolled, setIsOverscrolled] = useState(false)
|
||||||
|
// HACK: it reports a scroll pos of 0 for a tick when fetching finishes
|
||||||
|
// so paper over that by keeping it true for a bit -sfn
|
||||||
|
const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => scrollY.value < -5,
|
||||||
|
(value, prevValue) => {
|
||||||
|
if (value !== prevValue) {
|
||||||
|
runOnJS(setIsOverscrolled)(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [isAnimating, setIsAnimating] = useState(isFetching)
|
||||||
|
|
||||||
|
if (isFetching && !isAnimating) {
|
||||||
|
setIsAnimating(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFetching && isAnimating && !stickyIsOverscrolled) {
|
||||||
|
setIsAnimating(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAnimating
|
||||||
|
}
|
||||||
|
|
||||||
|
// stayed true for at least `delay` ms before returning to false
|
||||||
|
function useStickyToggle(value: boolean, delay: number) {
|
||||||
|
const [prevValue, setPrevValue] = useState(value)
|
||||||
|
const [isSticking, setIsSticking] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSticking) {
|
||||||
|
const timeout = setTimeout(() => setIsSticking(false), delay)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [isSticking, delay])
|
||||||
|
|
||||||
|
if (value !== prevValue) {
|
||||||
|
setIsSticking(prevValue) // Going true -> false should stick.
|
||||||
|
setPrevValue(value)
|
||||||
|
return prevValue ? true : value
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSticking ? true : value
|
||||||
|
}
|
||||||
@@ -6,19 +6,20 @@ 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 {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {ProfileImageLightbox, useLightboxControls} from '#/state/lightbox'
|
import {ProfileImageLightbox, useLightboxControls} from '#/state/lightbox'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||||
import {isIOS} from 'platform/detection'
|
|
||||||
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, 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 {GrowableBanner} from './GrowableBanner'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
@@ -63,11 +64,35 @@ let ProfileHeaderShell = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
<View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
<View pointerEvents={isIOS ? 'auto' : 'none'}>
|
<View
|
||||||
|
pointerEvents={isIOS ? 'auto' : 'none'}
|
||||||
|
style={[a.relative, {height: 150}]}>
|
||||||
|
<GrowableBanner
|
||||||
|
backButton={
|
||||||
|
<>
|
||||||
|
{!isDesktop && !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>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}>
|
||||||
{isPlaceholderProfile ? (
|
{isPlaceholderProfile ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
width="100%"
|
width="100%"
|
||||||
height={150}
|
height="100%"
|
||||||
style={{borderRadius: 0}}
|
style={{borderRadius: 0}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -77,6 +102,7 @@ let ProfileHeaderShell = ({
|
|||||||
moderation={moderation.ui('banner')}
|
moderation={moderation.ui('banner')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</GrowableBanner>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
@@ -93,19 +119,6 @@ let ProfileHeaderShell = ({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isDesktop && !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>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
)}
|
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
testID="profileHeaderAviButton"
|
testID="profileHeaderAviButton"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
@@ -144,6 +157,9 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 15,
|
borderRadius: 15,
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
backBtn: {
|
backBtn: {
|
||||||
width: 30,
|
width: 30,
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent} from 'state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'actor-starter-packs'
|
export const RQKEY_ROOT = 'actor-starter-packs'
|
||||||
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useActorStarterPacksQuery({did}: {did?: string}) {
|
export function useActorStarterPacksQuery({did}: {did?: string}) {
|
||||||
|
|||||||
@@ -15,24 +15,24 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {AuthorFeedAPI} from '#/lib/api/feed/author'
|
||||||
|
import {CustomFeedAPI} from '#/lib/api/feed/custom'
|
||||||
|
import {FollowingFeedAPI} from '#/lib/api/feed/following'
|
||||||
import {HomeFeedAPI} from '#/lib/api/feed/home'
|
import {HomeFeedAPI} from '#/lib/api/feed/home'
|
||||||
|
import {LikesFeedAPI} from '#/lib/api/feed/likes'
|
||||||
|
import {ListFeedAPI} from '#/lib/api/feed/list'
|
||||||
|
import {MergeFeedAPI} from '#/lib/api/feed/merge'
|
||||||
|
import {FeedAPI, ReasonFeedSource} from '#/lib/api/feed/types'
|
||||||
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
||||||
|
import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip'
|
||||||
import {DISCOVER_FEED_URI} from '#/lib/constants'
|
import {DISCOVER_FEED_URI} from '#/lib/constants'
|
||||||
|
import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
|
||||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import * as userActionHistory from '#/state/userActionHistory'
|
import * as userActionHistory from '#/state/userActionHistory'
|
||||||
import {AuthorFeedAPI} from 'lib/api/feed/author'
|
|
||||||
import {CustomFeedAPI} from 'lib/api/feed/custom'
|
|
||||||
import {FollowingFeedAPI} from 'lib/api/feed/following'
|
|
||||||
import {LikesFeedAPI} from 'lib/api/feed/likes'
|
|
||||||
import {ListFeedAPI} from 'lib/api/feed/list'
|
|
||||||
import {MergeFeedAPI} from 'lib/api/feed/merge'
|
|
||||||
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
|
||||||
import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip'
|
|
||||||
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
|
|
||||||
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
|
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
|
||||||
import {useFeedTuners} from '../preferences/feed-tuners'
|
import {useFeedTuners} from '../preferences/feed-tuners'
|
||||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||||
@@ -65,7 +65,7 @@ export interface FeedParams {
|
|||||||
|
|
||||||
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
|
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
|
||||||
|
|
||||||
const RQKEY_ROOT = 'post-feed'
|
export const RQKEY_ROOT = 'post-feed'
|
||||||
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
|
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
|
||||||
return [RQKEY_ROOT, feedDesc, params || {}]
|
return [RQKEY_ROOT, feedDesc, params || {}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const PAGE_SIZE = 50
|
|||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
// TODO refactor invalidate on mutate?
|
// TODO refactor invalidate on mutate?
|
||||||
const RQKEY_ROOT = 'profile-feedgens'
|
export const RQKEY_ROOT = 'profile-feedgens'
|
||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useProfileFeedgensQuery(
|
export function useProfileFeedgensQuery(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {useModerationOpts} from '../preferences/moderation-opts'
|
|||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
const RQKEY_ROOT = 'profile-lists'
|
export const RQKEY_ROOT = 'profile-lists'
|
||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, {useContext} from 'react'
|
||||||
|
import {SharedValue} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
|
|
||||||
|
export const PagerHeaderContext =
|
||||||
|
React.createContext<SharedValue<number> | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
export function PagerHeaderProvider({
|
||||||
|
scrollY,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
scrollY: SharedValue<number>
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<PagerHeaderContext.Provider value={scrollY}>
|
||||||
|
{children}
|
||||||
|
</PagerHeaderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePagerHeaderContext() {
|
||||||
|
const scrollY = useContext(PagerHeaderContext)
|
||||||
|
if (isIOS) {
|
||||||
|
if (!scrollY) {
|
||||||
|
throw new Error(
|
||||||
|
'usePagerHeaderContext must be used within a HeaderProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {scrollY}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import {ScrollProvider} from '#/lib/ScrollContext'
|
|||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
||||||
import {ListMethods} from '../util/List'
|
import {ListMethods} from '../util/List'
|
||||||
|
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||||
import {TabBar} from './TabBar'
|
import {TabBar} from './TabBar'
|
||||||
|
|
||||||
export interface PagerWithHeaderChildParams {
|
export interface PagerWithHeaderChildParams {
|
||||||
@@ -82,6 +83,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
const renderTabBar = React.useCallback(
|
const renderTabBar = React.useCallback(
|
||||||
(props: RenderTabBarFnProps) => {
|
(props: RenderTabBarFnProps) => {
|
||||||
return (
|
return (
|
||||||
|
<PagerHeaderProvider scrollY={scrollY}>
|
||||||
<PagerTabBar
|
<PagerTabBar
|
||||||
headerOnlyHeight={headerOnlyHeight}
|
headerOnlyHeight={headerOnlyHeight}
|
||||||
items={items}
|
items={items}
|
||||||
@@ -96,6 +98,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
testID={testID}
|
testID={testID}
|
||||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||||
/>
|
/>
|
||||||
|
</PagerHeaderProvider>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
bannerImage: {
|
bannerImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 150,
|
height: '100%',
|
||||||
},
|
},
|
||||||
defaultBanner: {
|
defaultBanner: {
|
||||||
backgroundColor: '#0070ff',
|
backgroundColor: '#0070ff',
|
||||||
|
|||||||
@@ -12165,6 +12165,11 @@ expo-asset@~10.0.6:
|
|||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
md5-file "^3.2.3"
|
md5-file "^3.2.3"
|
||||||
|
|
||||||
|
expo-blur@^13.0.2:
|
||||||
|
version "13.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-13.0.2.tgz#c2d179b19b13830db1d8b90c51373235f462e958"
|
||||||
|
integrity sha512-t2p7BChO3Reykued++QJRMZ/og6J3aXtSQ+bU31YcBeXhZLkHwjWEhiPKPnJka7J2/yTs4+jOCNDY0kCZmcE3w==
|
||||||
|
|
||||||
expo-build-properties@^0.12.1:
|
expo-build-properties@^0.12.1:
|
||||||
version "0.12.1"
|
version "0.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-0.12.1.tgz#8d11759b8f382e4654e2482ddcec4f9ad4530aad"
|
resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-0.12.1.tgz#8d11759b8f382e4654e2482ddcec4f9ad4530aad"
|
||||||
|
|||||||
Reference in New Issue
Block a user