diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index fe7b9b78a2..eec30ec404 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -1,6 +1,7 @@ import React, {MutableRefObject} from 'react' import { ActivityIndicator, + Dimensions, RefreshControl, StyleProp, View, @@ -18,7 +19,6 @@ import {ListModel} from 'state/models/content/list' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s} from 'lib/styles' import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {logger} from '#/logger' import {useModalControls} from '#/state/modals' @@ -226,7 +226,9 @@ export const ListItems = observer(function ListItemsImpl({ progressViewOffset={headerOffset} /> } - contentContainerStyle={s.contentContainer} + contentContainerStyle={{ + paddingBottom: Dimensions.get('window').height - headerOffset, + }} style={{paddingTop: headerOffset}} onScroll={scrollHandler} onEndReached={onEndReached} diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index 7ec2926670..3b5e9164a7 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -49,7 +49,18 @@ export const Pager = React.forwardRef(function PagerImpl( onSelect: onTabBarSelect, })} {React.Children.map(children, (child, i) => ( - + {child} ))} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 8b9e0c85ad..e93d91fedb 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,17 +1,19 @@ import * as React from 'react' import { LayoutChangeEvent, - NativeScrollEvent, + FlatList, + ScrollView, StyleSheet, View, + NativeScrollEvent, } from 'react-native' import Animated, { - Easing, - useAnimatedReaction, useAnimatedStyle, useSharedValue, - withTiming, runOnJS, + scrollTo, + useAnimatedRef, + AnimatedRef, } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' @@ -24,6 +26,7 @@ interface PagerWithHeaderChildParams { headerHeight: number onScroll: OnScrollHandler isScrolledDown: boolean + scrollElRef: React.MutableRefObject | ScrollView | null> } export interface PagerWithHeaderProps { @@ -54,28 +57,12 @@ export const PagerWithHeader = React.forwardRef( ) { const {isMobile} = useWebMediaQueries() const [currentPage, setCurrentPage] = React.useState(0) - const scrollYs = React.useRef>({}) - const scrollY = useSharedValue(scrollYs.current[currentPage] || 0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) - const [isScrolledDown, setIsScrolledDown] = React.useState( - scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT, - ) - + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const scrollY = useSharedValue(0) const headerHeight = headerOnlyHeight + tabBarHeight - // react to scroll updates - function onScrollUpdate(v: number) { - // track each page's current scroll position - scrollYs.current[currentPage] = Math.min(v, headerOnlyHeight) - // update the 'is scrolled down' value - setIsScrolledDown(v > SCROLLED_DOWN_LIMIT) - } - useAnimatedReaction( - () => scrollY.value, - v => runOnJS(onScrollUpdate)(v), - ) - // capture the header bar sizing const onTabBarLayout = React.useCallback( (evt: LayoutChangeEvent) => { @@ -91,19 +78,17 @@ export const PagerWithHeader = React.forwardRef( ) // render the the header and tab bar - const headerTransform = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: Math.min( - Math.min(scrollY.value, headerOnlyHeight) * -1, - 0, - ), - }, - ], - }), - [scrollY, headerHeight, tabBarHeight], - ) + const headerTransform = useAnimatedStyle(() => ({ + transform: [ + { + translateY: Math.min( + Math.min(scrollY.value, headerOnlyHeight) * -1, + 0, + ), + }, + ], + })) + const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( @@ -144,12 +129,38 @@ export const PagerWithHeader = React.forwardRef( ], ) - // props to pass into children render functions - function onScrollWorklet(e: NativeScrollEvent) { - 'worklet' - scrollY.value = e.contentOffset.y + const scrollRefs = useSharedValue[]>([]) + const registerRef = (scrollRef: AnimatedRef, index: number) => { + scrollRefs.modify(refs => { + 'worklet' + refs[index] = scrollRef + return refs + }) } + const onScrollWorklet = React.useCallback( + (e: NativeScrollEvent) => { + 'worklet' + const nextScrollY = e.contentOffset.y + scrollY.value = nextScrollY + + if (nextScrollY < headerOnlyHeight) { + const refs = scrollRefs.value + for (let i = 0; i < refs.length; i++) { + if (i !== currentPage) { + scrollTo(refs[i], 0, nextScrollY, false) + } + } + } + + const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT + if (isScrolledDown !== nextIsScrolledDown) { + runOnJS(setIsScrolledDown)(nextIsScrolledDown) + } + }, + [currentPage, headerOnlyHeight, isScrolledDown, scrollRefs, scrollY], + ) + const onPageSelectedInner = React.useCallback( (index: number) => { setCurrentPage(index) @@ -158,19 +169,9 @@ export const PagerWithHeader = React.forwardRef( [onPageSelected, setCurrentPage], ) - const onPageSelecting = React.useCallback( - (index: number) => { - setCurrentPage(index) - if (scrollY.value > headerHeight) { - scrollY.value = headerHeight - } - scrollY.value = withTiming(scrollYs.current[index] || 0, { - duration: 170, - easing: Easing.inOut(Easing.quad), - }) - }, - [scrollY, setCurrentPage, scrollYs, headerHeight], - ) + const onPageSelecting = React.useCallback((index: number) => { + setCurrentPage(index) + }, []) return ( ( {toArray(children) .filter(Boolean) .map((child, i) => { - let output = null - if ( - child != null && - // Defer showing content until we know it won't jump. - isHeaderReady && - headerOnlyHeight > 0 && - tabBarHeight > 0 - ) { - output = child({ - headerHeight, - isScrolledDown, - onScroll: { - onScroll: i === currentPage ? onScrollWorklet : noop, - }, - }) - } - // Pager children must be noncollapsible plain s. + const isReady = + isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 return ( - {output} + ) => registerRef(r, i)} + renderTab={child} + /> ) })} @@ -212,6 +205,43 @@ export const PagerWithHeader = React.forwardRef( }, ) +function PagerItem({ + headerHeight, + isReady, + isScrolledDown, + onScrollWorklet, + renderTab, + registerRef, +}: { + headerHeight: number + isReady: boolean + isScrolledDown: boolean + registerRef: (scrollRef: AnimatedRef) => void + onScrollWorklet: (e: NativeScrollEvent) => void + renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null +}) { + const scrollElRef = useAnimatedRef() + registerRef(scrollElRef) + + const scrollHandler = React.useMemo( + () => ({onScroll: onScrollWorklet}), + [onScrollWorklet], + ) + + if (!isReady || renderTab == null) { + return null + } + + return renderTab({ + headerHeight, + isScrolledDown, + onScroll: scrollHandler, + scrollElRef: scrollElRef as React.MutableRefObject< + FlatList | ScrollView | null + >, + }) +} + const styles = StyleSheet.create({ tabBarMobile: { position: 'absolute', diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 5b517f4c7e..23ab2a7ba6 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -2,6 +2,7 @@ import React, {MutableRefObject} from 'react' import {observer} from 'mobx-react-lite' import { ActivityIndicator, + Dimensions, RefreshControl, StyleProp, StyleSheet, @@ -15,7 +16,6 @@ import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' -import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' @@ -178,7 +178,9 @@ export const Feed = observer(function Feed({ progressViewOffset={headerOffset} /> } - contentContainerStyle={s.contentContainer} + contentContainerStyle={{ + paddingBottom: Dimensions.get('window').height - headerOffset, + }} style={{paddingTop: headerOffset}} onScroll={onScroll != null ? scrollHandler : undefined} scrollEventThrottle={scrollEventThrottle} diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index c1496e4adb..e1cc0e9380 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -1,5 +1,11 @@ import React, {useMemo, useCallback} from 'react' -import {FlatList, StyleSheet, View, ActivityIndicator} from 'react-native' +import { + Dimensions, + StyleSheet, + View, + ActivityIndicator, + FlatList, +} from 'react-native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useNavigation} from '@react-navigation/native' import {usePalette} from 'lib/hooks/usePalette' @@ -343,16 +349,19 @@ export const ProfileFeedScreenInner = observer( isHeaderReady={feedInfo?.hasLoaded ?? false} renderHeader={renderHeader} onCurrentPageSelected={onCurrentPageSelected}> - {({onScroll, headerHeight, isScrolledDown}) => ( + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( | null> + } /> )} - {({onScroll, headerHeight}) => ( + {({onScroll, headerHeight, scrollElRef}) => ( + } /> )} @@ -387,14 +399,14 @@ interface FeedSectionProps { onScroll: OnScrollHandler headerHeight: number isScrolledDown: boolean + scrollElRef: React.MutableRefObject | null> } const FeedSection = React.forwardRef( function FeedSectionImpl( - {feed, onScroll, headerHeight, isScrolledDown}, + {feed, onScroll, headerHeight, isScrolledDown, scrollElRef}, ref, ) { const hasNew = feed.hasNewLatest && !feed.isRefreshing - const scrollElRef = React.useRef(null) const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({offset: -headerHeight}) @@ -438,6 +450,7 @@ const AboutSection = observer(function AboutPageImpl({ headerHeight, onToggleLiked, onScroll, + scrollElRef, }: { feedOwnerDid: string feedRkey: string @@ -445,6 +458,7 @@ const AboutSection = observer(function AboutPageImpl({ headerHeight: number onToggleLiked: () => void onScroll: OnScrollHandler + scrollElRef: React.MutableRefObject }) { const pal = usePalette('default') const {_} = useLingui() @@ -456,8 +470,12 @@ const AboutSection = observer(function AboutPageImpl({ return ( - {({onScroll, headerHeight, isScrolledDown}) => ( + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( | null> + } feed={feed} onScroll={onScroll} headerHeight={headerHeight} isScrolledDown={isScrolledDown} /> )} - {({onScroll, headerHeight, isScrolledDown}) => ( + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( | null> + } list={list} descriptionRT={list.descriptionRT} creator={list.data ? list.data.creator : undefined} @@ -223,9 +229,12 @@ export const ProfileListScreenInner = observer( items={SECTION_TITLES_MOD} isHeaderReady={list.hasLoaded} renderHeader={renderHeader}> - {({onScroll, headerHeight, isScrolledDown}) => ( + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( | null> + } descriptionRT={list.descriptionRT} creator={list.data ? list.data.creator : undefined} isCurateList={list.isCuratelist} @@ -557,14 +566,14 @@ interface FeedSectionProps { onScroll: OnScrollHandler headerHeight: number isScrolledDown: boolean + scrollElRef: React.MutableRefObject | null> } const FeedSection = React.forwardRef( function FeedSectionImpl( - {feed, onScroll, headerHeight, isScrolledDown}, + {feed, scrollElRef, onScroll, headerHeight, isScrolledDown}, ref, ) { const hasNew = feed.hasNewLatest && !feed.isRefreshing - const scrollElRef = React.useRef(null) const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({offset: -headerHeight}) @@ -611,6 +620,7 @@ interface AboutSectionProps { onScroll: OnScrollHandler headerHeight: number isScrolledDown: boolean + scrollElRef: React.MutableRefObject | null> } const AboutSection = React.forwardRef( function AboutSectionImpl( @@ -624,13 +634,13 @@ const AboutSection = React.forwardRef( onScroll, headerHeight, isScrolledDown, + scrollElRef, }, ref, ) { const pal = usePalette('default') const {_} = useLingui() const {isMobile} = useWebMediaQueries() - const scrollElRef = React.useRef(null) const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({offset: -headerHeight})