diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index c335494001..5149ba6c3a 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -7,6 +7,11 @@ import { View, type ViewStyle, } from 'react-native' +import { + type ScrollHandler, + useAnimatedProps, + useSharedValue, +} from 'react-native-reanimated' import {type AppBskyGraphDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -18,9 +23,11 @@ import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' +import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext' import {parseStarterPackUri} from '#/lib/strings/starter-pack' import {logger} from '#/logger' import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' +import {useShellLayout} from '#/state/shell/shell-layout' import { EmptyState, type EmptyStateButtonProps, @@ -47,6 +54,7 @@ interface ProfileFeedgensProps { scrollElRef: ListRef did: string headerOffset: number + collapsedHeaderHeight?: number enabled?: boolean style?: StyleProp testID?: string @@ -66,6 +74,7 @@ export function ProfileStarterPacks({ scrollElRef, did, headerOffset, + collapsedHeaderHeight = 0, enabled, style, testID, @@ -79,6 +88,39 @@ export function ProfileStarterPacks({ const bottomBarOffset = useBottomBarOffset(100) const {height} = useWindowDimensions() const [isPTRing, setIsPTRing] = useState(false) + + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + + const scrollY = useSharedValue(0) + const onScrollWorklet = useCallback>( + (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 { data, refetch, @@ -161,30 +203,38 @@ export function ProfileStarterPacks({ return ( - + + + ) } diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index e4af8c4a86..8a8ecb92e1 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -1,5 +1,10 @@ import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react' import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native' +import { + type ScrollHandler, + useAnimatedProps, + useSharedValue, +} from 'react-native-reanimated' import { type AppBskyLabelerDefs, type InterpretedLabelValueDefinition, @@ -11,6 +16,8 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' 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 {atoms as a, ios, tokens, useTheme} from '#/alf' import {Divider} from '#/components/Divider' @@ -31,6 +38,7 @@ interface LabelsSectionProps { moderationOpts: ModerationOpts scrollElRef: ListRef headerHeight: number + collapsedHeaderHeight: number isFocused: boolean setScrollViewTag: (tag: number | null) => void } @@ -43,11 +51,45 @@ export function ProfileLabelsSection({ moderationOpts, scrollElRef, headerHeight, + collapsedHeaderHeight, isFocused, setScrollViewTag, }: LabelsSectionProps) { const t = useTheme() + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + + const scrollY = useSharedValue(0) + const onScrollWorklet = useCallback>( + (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(() => { scrollElRef.current?.scrollToOffset({ animated: IS_NATIVE, @@ -119,30 +161,38 @@ export function ProfileLabelsSection({ return ( - - } - ListFooterComponent={ - - } - /> + + + } + ListFooterComponent={ + + } + animatedProps={animatedProps} + /> + ) } diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 6eedcd8622..ef135e4be5 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -13,16 +13,23 @@ import { View, type ViewStyle, } from 'react-native' +import { + type ScrollHandler, + useAnimatedProps, + useSharedValue, +} from 'react-native-reanimated' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {usePreferencesQuery} from '#/state/queries/preferences' import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' import {useSession} from '#/state/session' +import {useShellLayout} from '#/state/shell/shell-layout' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListRef} from '#/view/com/util/List' @@ -48,6 +55,7 @@ interface ProfileFeedgensProps { did: string scrollElRef: ListRef headerOffset: number + collapsedHeaderHeight?: number enabled?: boolean style?: StyleProp testID?: string @@ -59,6 +67,7 @@ export function ProfileFeedgens({ did, scrollElRef, headerOffset, + collapsedHeaderHeight = 0, enabled, style, testID, @@ -67,6 +76,39 @@ export function ProfileFeedgens({ const {_} = useLingui() const t = useTheme() const [isPTRing, setIsPTRing] = useState(false) + + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + + const scrollY = useSharedValue(0) + const onScrollWorklet = useCallback>( + (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 opts = useMemo(() => ({enabled}), [enabled]) const { @@ -246,22 +288,30 @@ export function ProfileFeedgens({ return ( - + + + ) } diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index c175793624..f8c6c668a0 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -13,16 +13,23 @@ import { View, type ViewStyle, } from 'react-native' +import { + type ScrollHandler, + useAnimatedProps, + useSharedValue, +} from 'react-native-reanimated' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {usePreferencesQuery} from '#/state/queries/preferences' import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' import {useSession} from '#/state/session' +import {useShellLayout} from '#/state/shell/shell-layout' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListRef} from '#/view/com/util/List' @@ -48,6 +55,7 @@ interface ProfileListsProps { did: string scrollElRef: ListRef headerOffset: number + collapsedHeaderHeight?: number enabled?: boolean style?: StyleProp testID?: string @@ -59,6 +67,7 @@ export function ProfileLists({ did, scrollElRef, headerOffset, + collapsedHeaderHeight = 0, enabled, style, testID, @@ -67,6 +76,39 @@ export function ProfileLists({ const {_} = useLingui() const t = useTheme() const [isPTRing, setIsPTRing] = useState(false) + + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + + const scrollY = useSharedValue(0) + const onScrollWorklet = useCallback>( + (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 opts = useMemo(() => ({enabled}), [enabled]) const { @@ -245,22 +287,30 @@ export function ProfileLists({ return ( - + + + ) } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 0e79f2f3fe..cad2295055 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -393,7 +393,7 @@ function ProfileScreenLoaded({ renderHeader={renderHeader} allowHeaderOverScroll> {showFiltersTab - ? ({headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ) : null} {showListsTab && !!profile.associated?.labeler - ? ({headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( @@ -535,25 +537,27 @@ function ProfileScreenLoaded({ ) : null} {showFeedsTab - ? ({headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ) : null} {showStarterPacksTab - ? ({headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (