Add scroll-to-top for all screens (#177)

This commit is contained in:
Paul Frazee
2023-02-09 16:10:22 -06:00
committed by GitHub
parent b84c88cd21
commit 70b0dc070a
8 changed files with 60 additions and 22 deletions
+1
View File
@@ -658,6 +658,7 @@ export const mockedRootStore = {
profiles: mockedProfilesStore,
linkMetas: mockedLinkMetasStore,
log: mockedLogStore,
onScreenSoftReset: jest.fn().mockReturnValue({remove: jest.fn()}),
} as RootStoreModel
export const mockedProfileUiStore = {
+10
View File
@@ -166,6 +166,16 @@ export class RootStoreModel {
DeviceEventEmitter.emit('navigation')
}
// a "soft reset" typically means scrolling to top and loading latest
// but it can depend on the screen
onScreenSoftReset(handler: () => void): EmitterSubscription {
return DeviceEventEmitter.addListener('screen-soft-reset', handler)
}
emitScreenSoftReset() {
DeviceEventEmitter.emit('screen-soft-reset')
}
// background fetch
// =
// - we use this to poll for unread notifications, which is not "ideal" behavior but
+4 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'
import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite'
import {FlatList, StyleSheet, View} from 'react-native'
import {NotificationsViewModel} from '../../../state/models/notifications-view'
@@ -13,10 +13,12 @@ const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
export const Feed = observer(function Feed({
view,
scrollElRef,
onPressTryAgain,
onScroll,
}: {
view: NotificationsViewModel
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
onScroll?: OnScrollCb
}) {
@@ -66,6 +68,7 @@ export const Feed = observer(function Feed({
)}
{data && (
<FlatList
ref={scrollElRef}
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
+1 -3
View File
@@ -1,5 +1,4 @@
import React, {MutableRefObject} from 'react'
import {FlatList} from 'react-native'
import React from 'react'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {Home} from './screens/Home'
import {Contacts} from './screens/Contacts'
@@ -21,7 +20,6 @@ export type ScreenParams = {
navIdx: string
params: Record<string, any>
visible: boolean
scrollElRef?: MutableRefObject<FlatList<any> | undefined>
}
export type Route = [React.FC<ScreenParams>, string, IconProp, RegExp]
export type MatchResult = {
+9 -6
View File
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FlatList, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import useAppState from 'react-native-appstate-hook'
import LinearGradient from 'react-native-linear-gradient'
@@ -17,15 +17,12 @@ import {useAnalytics} from '@segment/analytics-react-native'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
export const Home = observer(function Home({
navIdx,
visible,
scrollElRef,
}: ScreenParams) {
export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
const store = useStores()
const onMainScroll = useOnMainScroll(store)
const {track} = useAnalytics()
const safeAreaInsets = useSafeAreaInsets()
const scrollElRef = React.useRef<FlatList>(null)
const [wasVisible, setWasVisible] = React.useState<boolean>(false)
const {appState} = useAppState({
onForeground: () => doPoll(true),
@@ -45,11 +42,17 @@ export const Home = observer(function Home({
[appState, visible, store],
)
const onSoftReset = () => {
scrollElRef.current?.scrollToOffset({offset: 0})
}
useEffect(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const feedCleanup = store.me.mainFeed.registerListeners()
const pollInterval = setInterval(() => doPoll(), 15e3)
const cleanup = () => {
clearInterval(pollInterval)
softResetSub.remove()
feedCleanup()
}
+13 -2
View File
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react'
import {View} from 'react-native'
import {FlatList, View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed'
import {useStores} from '../../state'
@@ -11,21 +11,31 @@ import {useAnalytics} from '@segment/analytics-react-native'
export const Notifications = ({navIdx, visible}: ScreenParams) => {
const store = useStores()
const onMainScroll = useOnMainScroll(store)
const scrollElRef = React.useRef<FlatList>(null)
const {screen} = useAnalytics()
useEffect(() => {
screen('Notifications')
}, [screen])
const onSoftReset = () => {
scrollElRef.current?.scrollToOffset({offset: 0})
}
useEffect(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
if (!visible) {
return
return cleanup
}
store.log.debug('Updating notifications feed')
store.me.notifications.update().then(() => {
store.me.notifications.updateReadState()
})
store.nav.setTitle(navIdx, 'Notifications')
return cleanup
}, [visible, store, navIdx])
const onPressTryAgain = () => {
@@ -39,6 +49,7 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
view={store.me.notifications}
onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll}
scrollElRef={scrollElRef}
/>
</View>
)
+12 -1
View File
@@ -29,6 +29,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics()
const scrollElRef = React.useRef<ScrollView>(null)
const textInput = React.useRef<TextInput>(null)
const [lastRenderTime, setRenderTime] = React.useState<number>(0) // used to trigger reloads
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
@@ -39,7 +40,16 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
)
const {name} = params
const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0})
}
React.useEffect(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
if (visible) {
const now = Date.now()
if (lastRenderTime - now > FIVE_MIN) {
@@ -49,6 +59,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
autocompleteView.setup()
store.nav.setTitle(navIdx, 'Search')
}
return cleanup
}, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
const onPressMenu = () => {
@@ -143,7 +154,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
</Text>
</View>
) : (
<ScrollView onScroll={Keyboard.dismiss}>
<ScrollView onScroll={Keyboard.dismiss} ref={scrollElRef}>
<WhoToFollow key={`wtf-${lastRenderTime}`} />
<SuggestedPosts key={`sp-${lastRenderTime}`} />
<View style={s.footerSpacer} />
+10 -9
View File
@@ -1,9 +1,8 @@
import React, {useState, useEffect, useRef} from 'react'
import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
Easing,
FlatList,
GestureResponderEvent,
StatusBar,
StyleSheet,
@@ -148,7 +147,6 @@ export const MobileShell: React.FC = observer(() => {
const pal = usePalette('default')
const store = useStores()
const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
const scrollElRef = useRef<FlatList | undefined>()
const winDim = useWindowDimensions()
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
const swipeGestureInterp = useAnimatedValue(0)
@@ -164,8 +162,8 @@ export const MobileShell: React.FC = observer(() => {
const onPressHome = () => {
track('MobileShell:HomeButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
if (store.nav.tab.current.url === '/') {
scrollElRef.current?.scrollToOffset({offset: 0})
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
@@ -179,8 +177,8 @@ export const MobileShell: React.FC = observer(() => {
const onPressSearch = () => {
track('MobileShell:SearchButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
if (store.nav.tab.current.url === '/') {
scrollElRef.current?.scrollToOffset({offset: 0})
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
@@ -194,7 +192,11 @@ export const MobileShell: React.FC = observer(() => {
const onPressNotifications = () => {
track('MobileShell:NotificationsButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
store.nav.tab.fixedTabReset()
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Notifs, false)
if (store.nav.tab.index === 0) {
@@ -444,7 +446,6 @@ export const MobileShell: React.FC = observer(() => {
params={params}
navIdx={navIdx}
visible={current}
scrollElRef={current ? scrollElRef : undefined}
/>
</ErrorBoundary>
</Animated.View>