fix: support scroll to top on profile screen (#725)
* Support scroll to top on profile screen * Refactor types * Remove async * Improve typeszio/stable
parent
792d7e1a55
commit
d4e7355cca
|
@ -13,104 +13,124 @@ const HEADER_ITEM = {_reactKey: '__header__'}
|
||||||
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
||||||
const STICKY_HEADER_INDICES = [1]
|
const STICKY_HEADER_INDICES = [1]
|
||||||
|
|
||||||
export function ViewSelector({
|
export type ViewSelectorHandle = {
|
||||||
sections,
|
scrollToTop: () => void
|
||||||
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 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({
|
export function Selector({
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
items,
|
items,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {observer} from 'mobx-react-lite'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
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 {CenteredView} from '../com/util/Views'
|
||||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||||
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
|
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
|
||||||
|
@ -35,6 +35,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
observer(({route}: Props) => {
|
observer(({route}: Props) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
|
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
screen('Profile')
|
screen('Profile')
|
||||||
|
@ -47,12 +48,17 @@ export const ProfileScreen = withAuthRequired(
|
||||||
)
|
)
|
||||||
useSetTitle(combinedDisplayName(uiState.profile))
|
useSetTitle(combinedDisplayName(uiState.profile))
|
||||||
|
|
||||||
|
const onSoftReset = React.useCallback(() => {
|
||||||
|
viewSelectorRef.current?.scrollToTop()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasSetup(false)
|
setHasSetup(false)
|
||||||
}, [route.params.name])
|
}, [route.params.name])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
|
const softResetSub = store.onScreenSoftReset(onSoftReset)
|
||||||
let aborted = false
|
let aborted = false
|
||||||
store.shell.setMinimalShellMode(false)
|
store.shell.setMinimalShellMode(false)
|
||||||
const feedCleanup = uiState.feed.registerListeners()
|
const feedCleanup = uiState.feed.registerListeners()
|
||||||
|
@ -69,8 +75,9 @@ export const ProfileScreen = withAuthRequired(
|
||||||
return () => {
|
return () => {
|
||||||
aborted = true
|
aborted = true
|
||||||
feedCleanup()
|
feedCleanup()
|
||||||
|
softResetSub.remove()
|
||||||
}
|
}
|
||||||
}, [hasSetup, uiState, store]),
|
}, [store, onSoftReset, uiState, hasSetup]),
|
||||||
)
|
)
|
||||||
|
|
||||||
// events
|
// events
|
||||||
|
@ -247,6 +254,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
/>
|
/>
|
||||||
) : uiState.profile.hasLoaded ? (
|
) : uiState.profile.hasLoaded ? (
|
||||||
<ViewSelector
|
<ViewSelector
|
||||||
|
ref={viewSelectorRef}
|
||||||
swipeEnabled={false}
|
swipeEnabled={false}
|
||||||
sections={uiState.selectorItems}
|
sections={uiState.selectorItems}
|
||||||
items={uiState.uiItems}
|
items={uiState.uiItems}
|
||||||
|
|
Loading…
Reference in New Issue