Add animated scroll indicator insets to remaining profile tabs
Apply the same scroll-position-aware indicator inset pattern to Labels, Lists, Feeds, and Starter Packs tabs. Each component now intercepts scroll events via ScrollProvider, tracks scrollY, and uses collapsedHeaderHeight to clamp the iOS scroll indicator top inset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,11 @@ import {
|
|||||||
View,
|
View,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
type ScrollHandler,
|
||||||
|
useAnimatedProps,
|
||||||
|
useSharedValue,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -18,9 +23,11 @@ import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
|||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
|
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
|
||||||
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
type EmptyStateButtonProps,
|
type EmptyStateButtonProps,
|
||||||
@@ -47,6 +54,7 @@ interface ProfileFeedgensProps {
|
|||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
did: string
|
did: string
|
||||||
headerOffset: number
|
headerOffset: number
|
||||||
|
collapsedHeaderHeight?: number
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
testID?: string
|
testID?: string
|
||||||
@@ -66,6 +74,7 @@ export function ProfileStarterPacks({
|
|||||||
scrollElRef,
|
scrollElRef,
|
||||||
did,
|
did,
|
||||||
headerOffset,
|
headerOffset,
|
||||||
|
collapsedHeaderHeight = 0,
|
||||||
enabled,
|
enabled,
|
||||||
style,
|
style,
|
||||||
testID,
|
testID,
|
||||||
@@ -79,6 +88,39 @@ export function ProfileStarterPacks({
|
|||||||
const bottomBarOffset = useBottomBarOffset(100)
|
const bottomBarOffset = useBottomBarOffset(100)
|
||||||
const {height} = useWindowDimensions()
|
const {height} = useWindowDimensions()
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
onBeginDrag: onBeginDragFromContext,
|
||||||
|
onEndDrag: onEndDragFromContext,
|
||||||
|
onScroll: onScrollFromContext,
|
||||||
|
onMomentumEnd: onMomentumEndFromContext,
|
||||||
|
} = useScrollHandlers()
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const onScrollWorklet = useCallback<ScrollHandler<any>>(
|
||||||
|
(e, ctx) => {
|
||||||
|
'worklet'
|
||||||
|
onScrollFromContext?.(e, ctx)
|
||||||
|
scrollY.set(e.contentOffset.y)
|
||||||
|
},
|
||||||
|
[onScrollFromContext, scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
|
const animatedProps = useAnimatedProps(() => {
|
||||||
|
if (IS_IOS) {
|
||||||
|
return {
|
||||||
|
scrollIndicatorInsets: {
|
||||||
|
top: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
|
||||||
|
right: 1,
|
||||||
|
left: 0,
|
||||||
|
bottom: footerHeight.get(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -161,30 +203,38 @@ export function ProfileStarterPacks({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
<List
|
<ScrollProvider
|
||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
onScroll={onScrollWorklet}
|
||||||
ref={scrollElRef}
|
onBeginDrag={onBeginDragFromContext}
|
||||||
data={items}
|
onEndDrag={onEndDragFromContext}
|
||||||
renderItem={renderItem}
|
onMomentumBegin={onMomentumEndFromContext}
|
||||||
keyExtractor={keyExtractor}
|
onMomentumEnd={onMomentumEndFromContext}>
|
||||||
refreshing={isPTRing}
|
<List
|
||||||
headerOffset={headerOffset}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
progressViewOffset={ios(0)}
|
ref={scrollElRef}
|
||||||
contentContainerStyle={{
|
data={items}
|
||||||
minHeight: height + headerOffset,
|
renderItem={renderItem}
|
||||||
paddingBottom: bottomBarOffset,
|
keyExtractor={keyExtractor}
|
||||||
}}
|
refreshing={isPTRing}
|
||||||
removeClippedSubviews={true}
|
headerOffset={headerOffset}
|
||||||
desktopFixedHeight
|
progressViewOffset={ios(0)}
|
||||||
onEndReached={onEndReached}
|
contentContainerStyle={{
|
||||||
onRefresh={onRefresh}
|
minHeight: height + headerOffset,
|
||||||
ListEmptyComponent={
|
paddingBottom: bottomBarOffset,
|
||||||
data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder
|
}}
|
||||||
}
|
removeClippedSubviews={true}
|
||||||
ListFooterComponent={
|
desktopFixedHeight
|
||||||
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
|
onEndReached={onEndReached}
|
||||||
}
|
onRefresh={onRefresh}
|
||||||
/>
|
ListEmptyComponent={
|
||||||
|
data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder
|
||||||
|
}
|
||||||
|
ListFooterComponent={
|
||||||
|
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||||
|
}
|
||||||
|
animatedProps={animatedProps}
|
||||||
|
/>
|
||||||
|
</ScrollProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react'
|
import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react'
|
||||||
import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native'
|
import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native'
|
||||||
|
import {
|
||||||
|
type ScrollHandler,
|
||||||
|
useAnimatedProps,
|
||||||
|
useSharedValue,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {
|
import {
|
||||||
type AppBskyLabelerDefs,
|
type AppBskyLabelerDefs,
|
||||||
type InterpretedLabelValueDefinition,
|
type InterpretedLabelValueDefinition,
|
||||||
@@ -11,6 +16,8 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
|
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
|
||||||
|
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
import {atoms as a, ios, tokens, useTheme} from '#/alf'
|
import {atoms as a, ios, tokens, useTheme} from '#/alf'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
@@ -31,6 +38,7 @@ interface LabelsSectionProps {
|
|||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
headerHeight: number
|
headerHeight: number
|
||||||
|
collapsedHeaderHeight: number
|
||||||
isFocused: boolean
|
isFocused: boolean
|
||||||
setScrollViewTag: (tag: number | null) => void
|
setScrollViewTag: (tag: number | null) => void
|
||||||
}
|
}
|
||||||
@@ -43,11 +51,45 @@ export function ProfileLabelsSection({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
|
collapsedHeaderHeight,
|
||||||
isFocused,
|
isFocused,
|
||||||
setScrollViewTag,
|
setScrollViewTag,
|
||||||
}: LabelsSectionProps) {
|
}: LabelsSectionProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {
|
||||||
|
onBeginDrag: onBeginDragFromContext,
|
||||||
|
onEndDrag: onEndDragFromContext,
|
||||||
|
onScroll: onScrollFromContext,
|
||||||
|
onMomentumEnd: onMomentumEndFromContext,
|
||||||
|
} = useScrollHandlers()
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const onScrollWorklet = useCallback<ScrollHandler<any>>(
|
||||||
|
(e, ctx) => {
|
||||||
|
'worklet'
|
||||||
|
onScrollFromContext?.(e, ctx)
|
||||||
|
scrollY.set(e.contentOffset.y)
|
||||||
|
},
|
||||||
|
[onScrollFromContext, scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
|
const animatedProps = useAnimatedProps(() => {
|
||||||
|
if (IS_IOS) {
|
||||||
|
return {
|
||||||
|
scrollIndicatorInsets: {
|
||||||
|
top: Math.max(headerHeight - scrollY.get(), collapsedHeaderHeight),
|
||||||
|
right: 1,
|
||||||
|
left: 0,
|
||||||
|
bottom: footerHeight.get(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({
|
scrollElRef.current?.scrollToOffset({
|
||||||
animated: IS_NATIVE,
|
animated: IS_NATIVE,
|
||||||
@@ -119,30 +161,38 @@ export function ProfileLabelsSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<List
|
<ScrollProvider
|
||||||
ref={scrollElRef}
|
onScroll={onScrollWorklet}
|
||||||
data={labelValues}
|
onBeginDrag={onBeginDragFromContext}
|
||||||
renderItem={renderItem}
|
onEndDrag={onEndDragFromContext}
|
||||||
keyExtractor={keyExtractor}
|
onMomentumBegin={onMomentumEndFromContext}
|
||||||
contentContainerStyle={a.px_xl}
|
onMomentumEnd={onMomentumEndFromContext}>
|
||||||
headerOffset={headerHeight}
|
<List
|
||||||
progressViewOffset={ios(0)}
|
ref={scrollElRef}
|
||||||
ListHeaderComponent={
|
data={labelValues}
|
||||||
<LabelerListHeader
|
renderItem={renderItem}
|
||||||
isLabelerLoading={isLabelerLoading}
|
keyExtractor={keyExtractor}
|
||||||
labelerInfo={labelerInfo}
|
contentContainerStyle={a.px_xl}
|
||||||
labelerError={labelerError}
|
headerOffset={headerHeight}
|
||||||
hasValues={labelValues.length !== 0}
|
progressViewOffset={ios(0)}
|
||||||
isSubscribed={isSubscribed}
|
ListHeaderComponent={
|
||||||
/>
|
<LabelerListHeader
|
||||||
}
|
isLabelerLoading={isLabelerLoading}
|
||||||
ListFooterComponent={
|
labelerInfo={labelerInfo}
|
||||||
<ListFooter
|
labelerError={labelerError}
|
||||||
height={headerHeight + 180}
|
hasValues={labelValues.length !== 0}
|
||||||
style={a.border_transparent}
|
isSubscribed={isSubscribed}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
ListFooterComponent={
|
||||||
|
<ListFooter
|
||||||
|
height={headerHeight + 180}
|
||||||
|
style={a.border_transparent}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
animatedProps={animatedProps}
|
||||||
|
/>
|
||||||
|
</ScrollProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ import {
|
|||||||
View,
|
View,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
type ScrollHandler,
|
||||||
|
useAnimatedProps,
|
||||||
|
useSharedValue,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
@@ -48,6 +55,7 @@ interface ProfileFeedgensProps {
|
|||||||
did: string
|
did: string
|
||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
headerOffset: number
|
headerOffset: number
|
||||||
|
collapsedHeaderHeight?: number
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
testID?: string
|
testID?: string
|
||||||
@@ -59,6 +67,7 @@ export function ProfileFeedgens({
|
|||||||
did,
|
did,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
headerOffset,
|
headerOffset,
|
||||||
|
collapsedHeaderHeight = 0,
|
||||||
enabled,
|
enabled,
|
||||||
style,
|
style,
|
||||||
testID,
|
testID,
|
||||||
@@ -67,6 +76,39 @@ export function ProfileFeedgens({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
onBeginDrag: onBeginDragFromContext,
|
||||||
|
onEndDrag: onEndDragFromContext,
|
||||||
|
onScroll: onScrollFromContext,
|
||||||
|
onMomentumEnd: onMomentumEndFromContext,
|
||||||
|
} = useScrollHandlers()
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const onScrollWorklet = useCallback<ScrollHandler<any>>(
|
||||||
|
(e, ctx) => {
|
||||||
|
'worklet'
|
||||||
|
onScrollFromContext?.(e, ctx)
|
||||||
|
scrollY.set(e.contentOffset.y)
|
||||||
|
},
|
||||||
|
[onScrollFromContext, scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
|
const animatedProps = useAnimatedProps(() => {
|
||||||
|
if (IS_IOS) {
|
||||||
|
return {
|
||||||
|
scrollIndicatorInsets: {
|
||||||
|
top: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
|
||||||
|
right: 1,
|
||||||
|
left: 0,
|
||||||
|
bottom: footerHeight.get(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
const {height} = useWindowDimensions()
|
const {height} = useWindowDimensions()
|
||||||
const opts = useMemo(() => ({enabled}), [enabled])
|
const opts = useMemo(() => ({enabled}), [enabled])
|
||||||
const {
|
const {
|
||||||
@@ -246,22 +288,30 @@ export function ProfileFeedgens({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
<List
|
<ScrollProvider
|
||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
onScroll={onScrollWorklet}
|
||||||
ref={scrollElRef}
|
onBeginDrag={onBeginDragFromContext}
|
||||||
data={items}
|
onEndDrag={onEndDragFromContext}
|
||||||
keyExtractor={keyExtractor}
|
onMomentumBegin={onMomentumEndFromContext}
|
||||||
renderItem={renderItem}
|
onMomentumEnd={onMomentumEndFromContext}>
|
||||||
ListFooterComponent={ProfileFeedgensFooter}
|
<List
|
||||||
refreshing={isPTRing}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
onRefresh={onRefresh}
|
ref={scrollElRef}
|
||||||
headerOffset={headerOffset}
|
data={items}
|
||||||
progressViewOffset={ios(0)}
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
renderItem={renderItem}
|
||||||
desktopFixedHeight
|
ListFooterComponent={ProfileFeedgensFooter}
|
||||||
onEndReached={onEndReached}
|
refreshing={isPTRing}
|
||||||
contentContainerStyle={{minHeight: height + headerOffset}}
|
onRefresh={onRefresh}
|
||||||
/>
|
headerOffset={headerOffset}
|
||||||
|
progressViewOffset={ios(0)}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
desktopFixedHeight
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
contentContainerStyle={{minHeight: height + headerOffset}}
|
||||||
|
animatedProps={animatedProps}
|
||||||
|
/>
|
||||||
|
</ScrollProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ import {
|
|||||||
View,
|
View,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
type ScrollHandler,
|
||||||
|
useAnimatedProps,
|
||||||
|
useSharedValue,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
|
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
@@ -48,6 +55,7 @@ interface ProfileListsProps {
|
|||||||
did: string
|
did: string
|
||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
headerOffset: number
|
headerOffset: number
|
||||||
|
collapsedHeaderHeight?: number
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
testID?: string
|
testID?: string
|
||||||
@@ -59,6 +67,7 @@ export function ProfileLists({
|
|||||||
did,
|
did,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
headerOffset,
|
headerOffset,
|
||||||
|
collapsedHeaderHeight = 0,
|
||||||
enabled,
|
enabled,
|
||||||
style,
|
style,
|
||||||
testID,
|
testID,
|
||||||
@@ -67,6 +76,39 @@ export function ProfileLists({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
onBeginDrag: onBeginDragFromContext,
|
||||||
|
onEndDrag: onEndDragFromContext,
|
||||||
|
onScroll: onScrollFromContext,
|
||||||
|
onMomentumEnd: onMomentumEndFromContext,
|
||||||
|
} = useScrollHandlers()
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const onScrollWorklet = useCallback<ScrollHandler<any>>(
|
||||||
|
(e, ctx) => {
|
||||||
|
'worklet'
|
||||||
|
onScrollFromContext?.(e, ctx)
|
||||||
|
scrollY.set(e.contentOffset.y)
|
||||||
|
},
|
||||||
|
[onScrollFromContext, scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
|
||||||
|
const animatedProps = useAnimatedProps(() => {
|
||||||
|
if (IS_IOS) {
|
||||||
|
return {
|
||||||
|
scrollIndicatorInsets: {
|
||||||
|
top: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
|
||||||
|
right: 1,
|
||||||
|
left: 0,
|
||||||
|
bottom: footerHeight.get(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
const {height} = useWindowDimensions()
|
const {height} = useWindowDimensions()
|
||||||
const opts = useMemo(() => ({enabled}), [enabled])
|
const opts = useMemo(() => ({enabled}), [enabled])
|
||||||
const {
|
const {
|
||||||
@@ -245,22 +287,30 @@ export function ProfileLists({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
<List
|
<ScrollProvider
|
||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
onScroll={onScrollWorklet}
|
||||||
ref={scrollElRef}
|
onBeginDrag={onBeginDragFromContext}
|
||||||
data={items}
|
onEndDrag={onEndDragFromContext}
|
||||||
keyExtractor={keyExtractor}
|
onMomentumBegin={onMomentumEndFromContext}
|
||||||
renderItem={renderItem}
|
onMomentumEnd={onMomentumEndFromContext}>
|
||||||
ListFooterComponent={ProfileListsFooter}
|
<List
|
||||||
refreshing={isPTRing}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
onRefresh={onRefresh}
|
ref={scrollElRef}
|
||||||
headerOffset={headerOffset}
|
data={items}
|
||||||
progressViewOffset={ios(0)}
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
renderItem={renderItem}
|
||||||
desktopFixedHeight
|
ListFooterComponent={ProfileListsFooter}
|
||||||
onEndReached={onEndReached}
|
refreshing={isPTRing}
|
||||||
contentContainerStyle={{minHeight: height + headerOffset}}
|
onRefresh={onRefresh}
|
||||||
/>
|
headerOffset={headerOffset}
|
||||||
|
progressViewOffset={ios(0)}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
desktopFixedHeight
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
contentContainerStyle={{minHeight: height + headerOffset}}
|
||||||
|
animatedProps={animatedProps}
|
||||||
|
/>
|
||||||
|
</ScrollProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ function ProfileScreenLoaded({
|
|||||||
renderHeader={renderHeader}
|
renderHeader={renderHeader}
|
||||||
allowHeaderOverScroll>
|
allowHeaderOverScroll>
|
||||||
{showFiltersTab
|
{showFiltersTab
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileLabelsSection
|
<ProfileLabelsSection
|
||||||
ref={labelsSectionRef}
|
ref={labelsSectionRef}
|
||||||
labelerInfo={labelerInfo}
|
labelerInfo={labelerInfo}
|
||||||
@@ -402,18 +402,20 @@ function ProfileScreenLoaded({
|
|||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
{showListsTab && !!profile.associated?.labeler
|
{showListsTab && !!profile.associated?.labeler
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileLists
|
<ProfileLists
|
||||||
ref={listsSectionRef}
|
ref={listsSectionRef}
|
||||||
did={profile.did}
|
did={profile.did}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||||
enabled={isFocused}
|
enabled={isFocused}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
/>
|
/>
|
||||||
@@ -535,25 +537,27 @@ function ProfileScreenLoaded({
|
|||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
{showFeedsTab
|
{showFeedsTab
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileFeedgens
|
<ProfileFeedgens
|
||||||
ref={feedsSectionRef}
|
ref={feedsSectionRef}
|
||||||
did={profile.did}
|
did={profile.did}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||||
enabled={isFocused}
|
enabled={isFocused}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
{showStarterPacksTab
|
{showStarterPacksTab
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileStarterPacks
|
<ProfileStarterPacks
|
||||||
ref={starterPacksSectionRef}
|
ref={starterPacksSectionRef}
|
||||||
did={profile.did}
|
did={profile.did}
|
||||||
isMe={isMe}
|
isMe={isMe}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||||
enabled={isFocused}
|
enabled={isFocused}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
emptyStateMessage={
|
emptyStateMessage={
|
||||||
@@ -579,12 +583,13 @@ function ProfileScreenLoaded({
|
|||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
{showListsTab && !profile.associated?.labeler
|
{showListsTab && !profile.associated?.labeler
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileLists
|
<ProfileLists
|
||||||
ref={listsSectionRef}
|
ref={listsSectionRef}
|
||||||
did={profile.did}
|
did={profile.did}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||||
enabled={isFocused}
|
enabled={isFocused}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user