Bluesky app fork with some witchin' additions 💫

fix: support scroll to top on profile screen (#725)

* Support scroll to top on profile screen

* Refactor types

* Remove async

* Improve types

authored by

Kadi Kraman and committed by
GitHub
d4e7355c 792d7e1a

+120 -92
+110 -90
src/view/com/util/ViewSelector.tsx
··· 13 13 const SELECTOR_ITEM = {_reactKey: '__selector__'} 14 14 const STICKY_HEADER_INDICES = [1] 15 15 16 - export function ViewSelector({ 17 - sections, 18 - items, 19 - refreshing, 20 - renderHeader, 21 - renderItem, 22 - ListFooterComponent, 23 - onSelectView, 24 - onScroll, 25 - onRefresh, 26 - onEndReached, 27 - }: { 28 - sections: string[] 29 - items: any[] 30 - refreshing?: boolean 31 - swipeEnabled?: boolean 32 - renderHeader?: () => JSX.Element 33 - renderItem: (item: any) => JSX.Element 34 - ListFooterComponent?: 35 - | React.ComponentType<any> 36 - | React.ReactElement 37 - | null 38 - | undefined 39 - onSelectView?: (viewIndex: number) => void 40 - onScroll?: OnScrollCb 41 - onRefresh?: () => void 42 - onEndReached?: (info: {distanceFromEnd: number}) => void 43 - }) { 44 - const pal = usePalette('default') 45 - const [selectedIndex, setSelectedIndex] = useState<number>(0) 16 + export type ViewSelectorHandle = { 17 + scrollToTop: () => void 18 + } 46 19 47 - // events 48 - // = 20 + export const ViewSelector = React.forwardRef< 21 + ViewSelectorHandle, 22 + { 23 + sections: string[] 24 + items: any[] 25 + refreshing?: boolean 26 + swipeEnabled?: boolean 27 + renderHeader?: () => JSX.Element 28 + renderItem: (item: any) => JSX.Element 29 + ListFooterComponent?: 30 + | React.ComponentType<any> 31 + | React.ReactElement 32 + | null 33 + | undefined 34 + onSelectView?: (viewIndex: number) => void 35 + onScroll?: OnScrollCb 36 + onRefresh?: () => void 37 + onEndReached?: (info: {distanceFromEnd: number}) => void 38 + } 39 + >( 40 + ( 41 + { 42 + sections, 43 + items, 44 + refreshing, 45 + renderHeader, 46 + renderItem, 47 + ListFooterComponent, 48 + onSelectView, 49 + onScroll, 50 + onRefresh, 51 + onEndReached, 52 + }, 53 + ref, 54 + ) => { 55 + const pal = usePalette('default') 56 + const [selectedIndex, setSelectedIndex] = useState<number>(0) 57 + const flatListRef = React.useRef<FlatList>(null) 49 58 50 - const keyExtractor = React.useCallback(item => item._reactKey, []) 59 + // events 60 + // = 51 61 52 - const onPressSelection = React.useCallback( 53 - (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), 54 - [setSelectedIndex, sections], 55 - ) 56 - useEffect(() => { 57 - onSelectView?.(selectedIndex) 58 - }, [selectedIndex, onSelectView]) 62 + const keyExtractor = React.useCallback(item => item._reactKey, []) 59 63 60 - // rendering 61 - // = 64 + const onPressSelection = React.useCallback( 65 + (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), 66 + [setSelectedIndex, sections], 67 + ) 68 + useEffect(() => { 69 + onSelectView?.(selectedIndex) 70 + }, [selectedIndex, onSelectView]) 62 71 63 - const renderItemInternal = React.useCallback( 64 - ({item}: {item: any}) => { 65 - if (item === HEADER_ITEM) { 66 - if (renderHeader) { 67 - return renderHeader() 72 + React.useImperativeHandle(ref, () => ({ 73 + scrollToTop: () => { 74 + flatListRef.current?.scrollToOffset({offset: 0}) 75 + }, 76 + })) 77 + 78 + // rendering 79 + // = 80 + 81 + const renderItemInternal = React.useCallback( 82 + ({item}: {item: any}) => { 83 + if (item === HEADER_ITEM) { 84 + if (renderHeader) { 85 + return renderHeader() 86 + } 87 + return <View /> 88 + } else if (item === SELECTOR_ITEM) { 89 + return ( 90 + <Selector 91 + items={sections} 92 + selectedIndex={selectedIndex} 93 + onSelect={onPressSelection} 94 + /> 95 + ) 96 + } else { 97 + return renderItem(item) 68 98 } 69 - return <View /> 70 - } else if (item === SELECTOR_ITEM) { 71 - return ( 72 - <Selector 73 - items={sections} 74 - selectedIndex={selectedIndex} 75 - onSelect={onPressSelection} 76 - /> 77 - ) 78 - } else { 79 - return renderItem(item) 80 - } 81 - }, 82 - [sections, selectedIndex, onPressSelection, renderHeader, renderItem], 83 - ) 99 + }, 100 + [sections, selectedIndex, onPressSelection, renderHeader, renderItem], 101 + ) 84 102 85 - const data = React.useMemo( 86 - () => [HEADER_ITEM, SELECTOR_ITEM, ...items], 87 - [items], 88 - ) 89 - return ( 90 - <FlatList 91 - data={data} 92 - keyExtractor={keyExtractor} 93 - renderItem={renderItemInternal} 94 - ListFooterComponent={ListFooterComponent} 95 - // NOTE sticky header disabled on android due to major performance issues -prf 96 - stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} 97 - onScroll={onScroll} 98 - onEndReached={onEndReached} 99 - refreshControl={ 100 - <RefreshControl 101 - refreshing={refreshing!} 102 - onRefresh={onRefresh} 103 - tintColor={pal.colors.text} 104 - /> 105 - } 106 - onEndReachedThreshold={0.6} 107 - contentContainerStyle={s.contentContainer} 108 - removeClippedSubviews={true} 109 - 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 110 - /> 111 - ) 112 - } 103 + const data = React.useMemo( 104 + () => [HEADER_ITEM, SELECTOR_ITEM, ...items], 105 + [items], 106 + ) 107 + return ( 108 + <FlatList 109 + ref={flatListRef} 110 + data={data} 111 + keyExtractor={keyExtractor} 112 + renderItem={renderItemInternal} 113 + ListFooterComponent={ListFooterComponent} 114 + // NOTE sticky header disabled on android due to major performance issues -prf 115 + stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} 116 + onScroll={onScroll} 117 + onEndReached={onEndReached} 118 + refreshControl={ 119 + <RefreshControl 120 + refreshing={refreshing!} 121 + onRefresh={onRefresh} 122 + tintColor={pal.colors.text} 123 + /> 124 + } 125 + onEndReachedThreshold={0.6} 126 + contentContainerStyle={s.contentContainer} 127 + removeClippedSubviews={true} 128 + 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 129 + /> 130 + ) 131 + }, 132 + ) 113 133 114 134 export function Selector({ 115 135 selectedIndex,
+10 -2
src/view/screens/Profile.tsx
··· 4 4 import {useFocusEffect} from '@react-navigation/native' 5 5 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 6 6 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 7 - import {ViewSelector} from '../com/util/ViewSelector' 7 + import {ViewSelector, ViewSelectorHandle} from '../com/util/ViewSelector' 8 8 import {CenteredView} from '../com/util/Views' 9 9 import {ScreenHider} from 'view/com/util/moderation/ScreenHider' 10 10 import {ProfileUiModel, Sections} from 'state/models/ui/profile' ··· 35 35 observer(({route}: Props) => { 36 36 const store = useStores() 37 37 const {screen, track} = useAnalytics() 38 + const viewSelectorRef = React.useRef<ViewSelectorHandle>(null) 38 39 39 40 useEffect(() => { 40 41 screen('Profile') ··· 47 48 ) 48 49 useSetTitle(combinedDisplayName(uiState.profile)) 49 50 51 + const onSoftReset = React.useCallback(() => { 52 + viewSelectorRef.current?.scrollToTop() 53 + }, []) 54 + 50 55 useEffect(() => { 51 56 setHasSetup(false) 52 57 }, [route.params.name]) 53 58 54 59 useFocusEffect( 55 60 React.useCallback(() => { 61 + const softResetSub = store.onScreenSoftReset(onSoftReset) 56 62 let aborted = false 57 63 store.shell.setMinimalShellMode(false) 58 64 const feedCleanup = uiState.feed.registerListeners() ··· 69 75 return () => { 70 76 aborted = true 71 77 feedCleanup() 78 + softResetSub.remove() 72 79 } 73 - }, [hasSetup, uiState, store]), 80 + }, [store, onSoftReset, uiState, hasSetup]), 74 81 ) 75 82 76 83 // events ··· 247 254 /> 248 255 ) : uiState.profile.hasLoaded ? ( 249 256 <ViewSelector 257 + ref={viewSelectorRef} 250 258 swipeEnabled={false} 251 259 sections={uiState.selectorItems} 252 260 items={uiState.uiItems}