From d4e7355cca05483bfba8c30dbb9b4cdd2be14e51 Mon Sep 17 00:00:00 2001 From: Kadi Kraman Date: Thu, 1 Jun 2023 18:00:00 +0200 Subject: [PATCH] fix: support scroll to top on profile screen (#725) * Support scroll to top on profile screen * Refactor types * Remove async * Improve types --- src/view/com/util/ViewSelector.tsx | 212 ++++++++++++++++------------- src/view/screens/Profile.tsx | 12 +- 2 files changed, 126 insertions(+), 98 deletions(-) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 5b671d06..705178a8 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -13,104 +13,124 @@ const HEADER_ITEM = {_reactKey: '__header__'} const SELECTOR_ITEM = {_reactKey: '__selector__'} const STICKY_HEADER_INDICES = [1] -export function ViewSelector({ - sections, - items, - refreshing, - renderHeader, - renderItem, - ListFooterComponent, - onSelectView, - onScroll, - onRefresh, - onEndReached, -}: { - sections: string[] - items: any[] - refreshing?: boolean - swipeEnabled?: boolean - renderHeader?: () => JSX.Element - renderItem: (item: any) => JSX.Element - ListFooterComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined - onSelectView?: (viewIndex: number) => void - onScroll?: OnScrollCb - onRefresh?: () => void - onEndReached?: (info: {distanceFromEnd: number}) => void -}) { - const pal = usePalette('default') - const [selectedIndex, setSelectedIndex] = useState(0) - - // events - // = - - const keyExtractor = React.useCallback(item => item._reactKey, []) - - const onPressSelection = React.useCallback( - (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), - [setSelectedIndex, sections], - ) - useEffect(() => { - onSelectView?.(selectedIndex) - }, [selectedIndex, onSelectView]) - - // rendering - // = - - const renderItemInternal = React.useCallback( - ({item}: {item: any}) => { - if (item === HEADER_ITEM) { - if (renderHeader) { - return renderHeader() - } - return - } else if (item === SELECTOR_ITEM) { - return ( - - ) - } else { - return renderItem(item) - } - }, - [sections, selectedIndex, onPressSelection, renderHeader, renderItem], - ) - - const data = React.useMemo( - () => [HEADER_ITEM, SELECTOR_ITEM, ...items], - [items], - ) - return ( - - } - onEndReachedThreshold={0.6} - contentContainerStyle={s.contentContainer} - removeClippedSubviews={true} - scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 - /> - ) +export type ViewSelectorHandle = { + scrollToTop: () => void } +export const ViewSelector = React.forwardRef< + ViewSelectorHandle, + { + sections: string[] + items: any[] + refreshing?: boolean + swipeEnabled?: boolean + renderHeader?: () => JSX.Element + renderItem: (item: any) => JSX.Element + ListFooterComponent?: + | React.ComponentType + | React.ReactElement + | null + | undefined + onSelectView?: (viewIndex: number) => void + onScroll?: OnScrollCb + onRefresh?: () => void + onEndReached?: (info: {distanceFromEnd: number}) => void + } +>( + ( + { + sections, + items, + refreshing, + renderHeader, + renderItem, + ListFooterComponent, + onSelectView, + onScroll, + onRefresh, + onEndReached, + }, + ref, + ) => { + const pal = usePalette('default') + const [selectedIndex, setSelectedIndex] = useState(0) + const flatListRef = React.useRef(null) + + // events + // = + + const keyExtractor = React.useCallback(item => item._reactKey, []) + + const onPressSelection = React.useCallback( + (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), + [setSelectedIndex, sections], + ) + useEffect(() => { + onSelectView?.(selectedIndex) + }, [selectedIndex, onSelectView]) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: () => { + flatListRef.current?.scrollToOffset({offset: 0}) + }, + })) + + // rendering + // = + + const renderItemInternal = React.useCallback( + ({item}: {item: any}) => { + if (item === HEADER_ITEM) { + if (renderHeader) { + return renderHeader() + } + return + } else if (item === SELECTOR_ITEM) { + return ( + + ) + } else { + return renderItem(item) + } + }, + [sections, selectedIndex, onPressSelection, renderHeader, renderItem], + ) + + const data = React.useMemo( + () => [HEADER_ITEM, SELECTOR_ITEM, ...items], + [items], + ) + return ( + + } + onEndReachedThreshold={0.6} + contentContainerStyle={s.contentContainer} + removeClippedSubviews={true} + scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 + /> + ) + }, +) + export function Selector({ selectedIndex, items, diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index a34ceb32..c5ad286c 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -4,7 +4,7 @@ import {observer} from 'mobx-react-lite' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ViewSelector} from '../com/util/ViewSelector' +import {ViewSelector, ViewSelectorHandle} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' import {ProfileUiModel, Sections} from 'state/models/ui/profile' @@ -35,6 +35,7 @@ export const ProfileScreen = withAuthRequired( observer(({route}: Props) => { const store = useStores() const {screen, track} = useAnalytics() + const viewSelectorRef = React.useRef(null) useEffect(() => { screen('Profile') @@ -47,12 +48,17 @@ export const ProfileScreen = withAuthRequired( ) useSetTitle(combinedDisplayName(uiState.profile)) + const onSoftReset = React.useCallback(() => { + viewSelectorRef.current?.scrollToTop() + }, []) + useEffect(() => { setHasSetup(false) }, [route.params.name]) useFocusEffect( React.useCallback(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) let aborted = false store.shell.setMinimalShellMode(false) const feedCleanup = uiState.feed.registerListeners() @@ -69,8 +75,9 @@ export const ProfileScreen = withAuthRequired( return () => { aborted = true feedCleanup() + softResetSub.remove() } - }, [hasSetup, uiState, store]), + }, [store, onSoftReset, uiState, hasSetup]), ) // events @@ -247,6 +254,7 @@ export const ProfileScreen = withAuthRequired( /> ) : uiState.profile.hasLoaded ? (