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

* Support scroll to top on profile screen

* Refactor types

* Remove async

* Improve types
zio/stable
Kadi Kraman 2023-06-01 18:00:00 +02:00 committed by GitHub
parent 792d7e1a55
commit d4e7355cca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 126 additions and 98 deletions

View File

@ -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<any>
| 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<number>(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 <View />
} else if (item === SELECTOR_ITEM) {
return (
<Selector
items={sections}
selectedIndex={selectedIndex}
onSelect={onPressSelection}
/>
)
} else {
return renderItem(item)
}
},
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
)
const data = React.useMemo(
() => [HEADER_ITEM, SELECTOR_ITEM, ...items],
[items],
)
return (
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
// NOTE sticky header disabled on android due to major performance issues -prf
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
onScroll={onScroll}
onEndReached={onEndReached}
refreshControl={
<RefreshControl
refreshing={refreshing!}
onRefresh={onRefresh}
tintColor={pal.colors.text}
/>
}
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<any>
| 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<number>(0)
const flatListRef = React.useRef<FlatList>(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 <View />
} else if (item === SELECTOR_ITEM) {
return (
<Selector
items={sections}
selectedIndex={selectedIndex}
onSelect={onPressSelection}
/>
)
} else {
return renderItem(item)
}
},
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
)
const data = React.useMemo(
() => [HEADER_ITEM, SELECTOR_ITEM, ...items],
[items],
)
return (
<FlatList
ref={flatListRef}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
// NOTE sticky header disabled on android due to major performance issues -prf
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
onScroll={onScroll}
onEndReached={onEndReached}
refreshControl={
<RefreshControl
refreshing={refreshing!}
onRefresh={onRefresh}
tintColor={pal.colors.text}
/>
}
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,

View File

@ -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<ViewSelectorHandle>(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 ? (
<ViewSelector
ref={viewSelectorRef}
swipeEnabled={false}
sections={uiState.selectorItems}
items={uiState.uiItems}